def _assert(ph: Phonopy, ref_vals, show=False): dynmat = ph.dynamical_matrix ddynmat = DerivativeOfDynamicalMatrix(dynmat) ddynmat.run([0, 0.1, 0.1]) ddm = ddynmat.d_dynamical_matrix condition = np.abs(ddm) > 1e-8 vals = np.extract(condition, ddm).real if show: _show(vals) np.testing.assert_allclose(vals, ref_vals, rtol=0, atol=1e-7)
def _get_dD(q, ddm: DerivativeOfDynamicalMatrix, perturbation): """Return q-vector derivative of dynamical matrix. Returns ------- shape=(3, num_band, num_band). """ ddm.run(q) ddm_vals = ddm.get_derivative_of_dynamical_matrix() dD = np.zeros(ddm_vals.shape[1:], dtype=ddm_vals.dtype, order="C") if len(ddm_vals) == 3: for i in range(3): dD += perturbation[i] * ddm_vals[i] return dD / np.linalg.norm(perturbation) else: dD += perturbation[0] * perturbation[0] * ddm_vals[0] dD += perturbation[1] * perturbation[1] * ddm_vals[1] dD += perturbation[2] * perturbation[2] * ddm_vals[2] dD += 2 * perturbation[0] * perturbation[1] * ddm_vals[5] dD += 2 * perturbation[0] * perturbation[2] * ddm_vals[4] dD += 2 * perturbation[1] * perturbation[2] * ddm_vals[3] return dD / np.linalg.norm(perturbation)**2
class GroupVelocity: """ d omega ---- ------- = \ / omega d q \/q Gradient of omega in reciprocal space. d D(q) <e(q,nu)|------|e(q,nu)> d q """ def __init__(self, dynamical_matrix, q_points=None, symmetry=None, q_length=None, frequency_factor_to_THz=VaspToTHz): """ q_points is a list of sets of q-point and q-direction: [[q-point, q-direction], [q-point, q-direction], ...] q_length is used such as D(q + q_length) - D(q - q_length). """ self._dynmat = dynamical_matrix primitive = dynamical_matrix.get_primitive() self._reciprocal_lattice_inv = primitive.get_cell() self._reciprocal_lattice = np.linalg.inv(self._reciprocal_lattice_inv) self._q_points = q_points self._q_length = q_length self._symmetry = symmetry self._perturation = None if q_length is None: self._ddm = DerivativeOfDynamicalMatrix(dynamical_matrix) else: self._ddm = None self._factor = frequency_factor_to_THz self._directions = np.array([[1, 2, 3], [1, 0, 0], [0, 1, 0], [0, 0, 1]], dtype='double') self._directions[0] /= np.linalg.norm(self._directions[0]) self._group_velocity = None if self._q_points is not None: self._set_group_velocity() def set_q_points(self, q_points, perturbation = None, phonons=None): self._q_points = q_points self._perturbation = perturbation if perturbation is None: self._directions[0] = np.array([1, 2, 3]) else: self._directions[0] = np.dot( self._reciprocal_lattice, perturbation) self._directions[0] /= np.linalg.norm(self._directions[0]) self._set_group_velocity(phonons=phonons) def set_q_length(self, q_length): self._q_length = q_length def get_group_velocity(self): return self._group_velocity def _set_group_velocity(self, phonons=None): v_g = [] if phonons is None: phonons = [None for q in self._q_points] for i, q in enumerate(self._q_points): dD_at_q = self._set_group_velocity_at_q(q, phonon=phonons[i]) v_g.append(dD_at_q) self._group_velocity = np.array(v_g) def _set_group_velocity_at_q(self, q, phonon=None): # phonon: freq, eigenvector, degenerate self._dynmat.set_dynamical_matrix(q) dm = self._dynmat.get_dynamical_matrix() if phonon is not None: freqs, eigvecs, degenerates = phonon uniq = np.unique(degenerates) deg_sets = [np.where(degenerates == ele)[0] for ele in uniq] else: eigvals, eigvecs = np.linalg.eigh(dm) eigvals = eigvals.real freqs = np.sqrt(abs(eigvals)) * np.sign(eigvals) * self._factor deg_sets = degenerate_sets(freqs) gv = np.zeros((len(freqs), 3), dtype='double') ddms = self._get_dD(np.array(q)) for deg in deg_sets: gv[deg] = self._perturb_D(ddms, eigvecs[:, deg]) for i in range(3): gv[:, i] *= self._factor ** 2 / freqs / 2 if self._perturbation is None: return self._symmetrize_group_velocity(gv, q) else: return gv def _get_dD(self, q): if self._q_length is None: return self._get_dD_analytical(q) else: return self._get_dD_FD(q) def _symmetrize_group_velocity(self, gv, q): rotations = [] for r in self._symmetry.get_reciprocal_operations(): q_in_BZ = q - np.rint(q) diff = q_in_BZ - np.dot(r, q_in_BZ) if (np.abs(diff) < self._symmetry.get_symmetry_tolerance()).all(): rotations.append(r) gv_sym = np.zeros_like(gv) for r in rotations: r_cart = similarity_transformation(self._reciprocal_lattice, r) gv_sym += np.dot(r_cart, gv.T).T return gv_sym / len(rotations) def _get_dD_FD(self, q): # finite difference ddm = [] for dqc in self._directions * self._q_length: dq = np.dot(self._reciprocal_lattice_inv, dqc) ddm.append(delta_dynamical_matrix(q, dq, self._dynmat) / self._q_length / 2) return np.array(ddm) def _get_dD_analytical(self, q): self._ddm.run(q) ddm = self._ddm.get_derivative_of_dynamical_matrix() ddm_dirs = np.zeros((len(self._directions),) + ddm.shape[1:], dtype='complex128') for i, dq in enumerate(self._directions): for j in range(3): ddm_dirs[i] += dq[j] * ddm[j] return ddm_dirs def _perturb_D(self, ddms, eigsets): eigvals, eigvecs = np.linalg.eigh( np.dot(eigsets.T.conj(), np.dot(ddms[0], eigsets))) gv = [] rot_eigsets = np.dot(eigsets, eigvecs) for ddm in ddms[1:]: gv.append( np.diag(np.dot(rot_eigsets.T.conj(), np.dot(ddm, rot_eigsets))).real) return np.transpose(gv)
class IrReps: def __init__(self, dynamical_matrix, q, is_little_cogroup=False, nac_q_direction=None, factor=VaspToTHz, symprec=1e-5, degeneracy_tolerance=1e-5, log_level=0): self._is_little_cogroup = is_little_cogroup self._nac_q_direction = nac_q_direction self._factor = factor self._log_level = log_level self._q = np.array(q) self._degeneracy_tolerance = degeneracy_tolerance self._symprec = symprec self._primitive = dynamical_matrix.get_primitive() self._dynamical_matrix = dynamical_matrix self._ddm = DerivativeOfDynamicalMatrix(dynamical_matrix) def run(self): self._set_eigenvectors(self._dynamical_matrix) self._symmetry_dataset = Symmetry(self._primitive, self._symprec).get_dataset() if not self._is_primitive_cell(): print('') print("Non-primitve cell is used.") print("Your unit cell may be transformed to a primitive cell " "by PRIMITIVE_AXIS tag.") return False (self._rotations_at_q, self._translations_at_q) = self._get_rotations_at_q() self._g = len(self._rotations_at_q) (self._pointgroup_symbol, self._transformation_matrix, self._conventional_rotations) = self._get_conventional_rotations() self._ground_matrices = self._get_ground_matrix() self._degenerate_sets = self._get_degenerate_sets() self._irreps = self._get_irreps() self._characters, self._irrep_dims = self._get_characters() self._ir_labels = None if self._pointgroup_symbol in character_table.keys(): self._rotation_symbols = self._get_rotation_symbols() if (abs(self._q) < self._symprec).all() and self._rotation_symbols: self._ir_labels = self._get_ir_labels() elif (abs(self._q) < self._symprec).all(): if self._log_level > 0: print("Database for this point group is not preprared.") else: if self._log_level > 0: print("Database for non-Gamma point is not prepared.") else: self._rotation_symbols = None return True def _get_degenerate_sets(self): deg_sets = get_degenerate_sets(self._freqs, cutoff=self._degeneracy_tolerance) self._ddm.run(self._q) ddm_q = np.sum([ self._ddm.get_derivative_of_dynamical_matrix()[i] * self._q[i] for i in range(3) ], axis=0) return deg_sets def get_band_indices(self): return self._degenerate_sets def get_characters(self): return self._characters def get_eigenvectors(self): return self._eigvecs def get_irreps(self): return self._irreps def get_ground_matrices(self): return self._ground_matrices def get_rotation_symbols(self): return self._rotation_symbols def get_rotations(self): return self._conventional_rotations def get_projection_operators(self, idx_irrep, i=None, j=None): if i is None or j is None: return self._get_character_projection_operators(idx_irrep) else: return self._get_projection_operators(idx_irrep, i, j) def show(self, show_irreps=False): self._show(show_irreps) def write_yaml(self, show_irreps=False): self._write_yaml(show_irreps) def _set_eigenvectors(self, dm): if self._nac_q_direction is not None and (np.abs(self._q) < 1e-5).all(): dm.set_dynamical_matrix(self._q, q_direction=self._nac_q_direction) else: dm.set_dynamical_matrix(self._q) eigvals, self._eigvecs = np.linalg.eigh(dm.get_dynamical_matrix()) self._freqs = np.sqrt(abs(eigvals)) * np.sign(eigvals) * self._factor def _get_rotations_at_q(self): rotations_at_q = [] trans_at_q = [] for r, t in zip(self._symmetry_dataset['rotations'], self._symmetry_dataset['translations']): # Using r is used instead of np.linalg.inv(r) diff = np.dot(self._q, r) - self._q if (abs(diff - np.rint(diff)) < self._symprec).all(): rotations_at_q.append(r) for i in range(3): if np.abs(t[i] - 1) < self._symprec: t[i] = 0.0 trans_at_q.append(t) return np.array(rotations_at_q), np.array(trans_at_q) def _get_conventional_rotations(self): spacegroup_symbol = self._symmetry_dataset['international'][0] spacegroup_number = self._symmetry_dataset['number'] rotations = self._rotations_at_q.copy() pointgroup = get_pointgroup(rotations) pointgroup_symbol = pointgroup[0] transformation_matrix = pointgroup[1] conventional_rotations = self._transform_rotations( transformation_matrix, rotations) return (pointgroup_symbol, transformation_matrix, conventional_rotations) def _transform_rotations(self, tmat, rotations): trans_rots = [] for r in rotations: r_conv = similarity_transformation(np.linalg.inv(tmat), r) trans_rots.append(np.rint(r_conv).astype(int)) return np.array(trans_rots) def _get_ground_matrix(self): matrices = [] for (r, t) in zip(self._rotations_at_q, self._translations_at_q): lat = self._primitive.get_cell().T r_cart = similarity_transformation(lat, r) perm_mat = self._get_modified_permutation_matrix(r, t) matrices.append(np.kron(perm_mat, r_cart)) return np.array(matrices) def _get_characters(self): characters = [] irrep_dims = [] for irrep_Rs in self._irreps: characters.append([np.trace(rep) for rep in irrep_Rs]) irrep_dims.append(len(irrep_Rs[0])) return np.array(characters), np.array(irrep_dims) def _get_modified_permutation_matrix( self, r, t, ): num_atom = self._primitive.get_number_of_atoms() pos = self._primitive.get_scaled_positions() matrix = np.zeros((num_atom, num_atom), dtype=complex) for i, p1 in enumerate(pos): p_rot = np.dot(r, p1) + t for j, p2 in enumerate(pos): diff = p_rot - p2 if (abs(diff - np.rint(diff)) < self._symprec).all(): # For this phase factor, see # Dynamics of perfect crystals by G. Venkataraman et al., # pp132 Eq. (3.22). # It is assumed that dynamical matrix is built without # considering internal atomic positions, so # the phase factors of eigenvectors are shifted in # _get_irreps(). phase_factor = np.dot(self._q, np.dot(np.linalg.inv(r), p2 - p_rot)) # This phase factor comes from non-pure-translation of # each symmetry opration. if self._is_little_cogroup: phase_factor += np.dot(t, self._q) matrix[j, i] = np.exp(2j * np.pi * phase_factor) return matrix def _get_irreps(self): eigvecs = [] phases = np.kron([ np.exp(2j * np.pi * np.dot(self._q, pos)) for pos in self._primitive.get_scaled_positions() ], [1, 1, 1]) for vec in self._eigvecs.T: eigvecs.append(vec * phases) irrep = [] for band_indices in self._degenerate_sets: irrep_Rs = [] for mat in self._ground_matrices: l = len(band_indices) if l == 1: vec = eigvecs[band_indices[0]] irrep_Rs.append([[np.vdot(vec, np.dot(mat, vec))]]) continue irrep_R = np.zeros((l, l), dtype=complex) for i, b_i in enumerate(band_indices): vec_i = eigvecs[b_i] for j, b_j in enumerate(band_indices): vec_j = eigvecs[b_j] irrep_R[i, j] = np.vdot(vec_i, np.dot(mat, vec_j)) irrep_Rs.append(irrep_R) irrep.append(irrep_Rs) return irrep def _get_character_projection_operators(self, idx_irrep): dim = self._irrep_dims[idx_irrep] chars = self._characters[idx_irrep] return np.sum([ mat * char.conj() for mat, char in zip(self._ground_matrices, chars) ], axis=0) * dim / self._g def _get_projection_operators(self, idx_irrep, i, j): dim = self._irrep_dims[idx_irrep] return np.sum([ mat * r[i, j].conj() for mat, r in zip(self._ground_matrices, self._irreps[idx_irrep]) ], axis=0) * dim / self._g def _get_rotation_symbols(self): ptg = self._pointgroup_symbol for mapping_table in character_table[ptg]['mapping_table']: rotation_symbols = [] for r in self._conventional_rotations: symbol = get_rotation_symbol(r, mapping_table) rotation_symbols.append(symbol) if not False in rotation_symbols: break if False in rotation_symbols: return None else: return rotation_symbols def _get_ir_labels(self): ir_labels = [] rot_list = character_table[self._pointgroup_symbol]['rotation_list'] char_table = character_table[ self._pointgroup_symbol]['character_table'] for chars, deg_set in zip(self._characters, self._degenerate_sets): chars_ordered = np.zeros(len(rot_list), dtype=complex) for rs, ch in zip(self._rotation_symbols, chars): chars_ordered[rot_list.index(rs)] += ch for i, rl in enumerate(rot_list): chars_ordered[i] /= len(character_table[ self._pointgroup_symbol]['mapping_table'][0][rl]) found = False for ct_label in char_table.keys(): if (abs(chars_ordered - np.array(char_table[ct_label])) < self._symprec).all(): ir_labels.append(ct_label) found = True break if not found: ir_labels.append(None) if self._log_level > 1: text = "" for v in chars_ordered: text += "%5.2f " % abs(v) if found: print("%s %s" % (text, ct_label)) else: print("%s Not found" % text) return ir_labels def _is_primitive_cell(self): num_identity = 0 for r in self._symmetry_dataset['rotations']: if (r - np.eye(3, dtype='intc') == 0).all(): num_identity += 1 if num_identity > 1: return False else: return True def _show(self, show_irreps): print('') print("-------------------------------") print(" Irreducible representations") print("-------------------------------") print("q-point: %s" % self._q) print("Point group: %s" % self._pointgroup_symbol) print('') if (np.abs(self._q) < self._symprec).all(): width = 6 print("Original rotation matrices:") print('') print_rotations(self._rotations_at_q, width=width) else: width = 4 print("Original symmetry operations:") print('') print_rotations(self._rotations_at_q, translations=self._translations_at_q, width=width) print("Transformation matrix:") print('') for v in self._transformation_matrix: print("%6.3f %6.3f %6.3f" % tuple(v)) print('') print("Rotation matrices by transformation matrix:") print('') print_rotations(self._conventional_rotations, rotation_symbols=self._rotation_symbols, width=width) print("Character table:") print('') for i, deg_set in enumerate(self._degenerate_sets): text = "%3d (%8.3f): " % (deg_set[0] + 1, self._freqs[deg_set[0]]) if self._ir_labels is None: print(text) else: print(text + self._ir_labels[i]) print_characters(self._characters[i]) print('') if show_irreps: self._show_irreps() def _show_irreps(self): print("IR representations:") print('') for i, (deg_set, irrep_Rs) in enumerate(zip(self._degenerate_sets, self._irreps)): print("%3d (%8.3f):" % (deg_set[0] + 1, self._freqs[deg_set[0]])) print('') for j, irrep_R in enumerate(irrep_Rs): for k in range(len(irrep_R)): text = " " for l in range(len(irrep_R)): if irrep_R[k][l].real > 0: sign_r = " " else: sign_r = "-" if irrep_R[k][l].imag > 0: sign_i = "+" else: sign_i = "-" if k == 0: str_index = "%2d" % (j + 1) else: str_index = " " if l > 0: str_index = '' text += "%s (%s%5.3f %s%5.3fi) " % ( str_index, sign_r, abs(irrep_R[k][l].real), sign_i, abs(irrep_R[k][l].imag)) print(text) if len(irrep_R) > 1: print('') if len(irrep_R) == 1: print('') def _write_yaml(self, show_irreps): w = open("irreps.yaml", 'w') w.write("q-position: [ %12.7f, %12.7f, %12.7f ]\n" % tuple(self._q)) w.write("point_group: %s\n" % self._pointgroup_symbol) w.write("transformation_matrix:\n") for v in self._transformation_matrix: w.write("- [ %10.7f, %10.7f, %10.7f ]\n" % tuple(v)) w.write("rotations:\n") for i, r in enumerate(self._conventional_rotations): w.write("- matrix:\n") for v in r: w.write(" - [ %2d, %2d, %2d ]\n" % tuple(v)) if self._rotation_symbols: w.write(" symbol: %s\n" % self._rotation_symbols[i]) w.write("normal_modes:\n") for i, deg_set in enumerate(self._degenerate_sets): w.write("- band_indices: [ ") w.write("%d" % (deg_set[0] + 1)) for bi in deg_set[1:]: w.write(", %d" % (bi + 1)) w.write(" ]\n") w.write(" frequency: %-15.10f\n" % self._freqs[deg_set[0]]) if self._ir_labels: w.write(" ir_label: %s\n" % self._ir_labels[i]) w.write(" characters: ") chars = np.rint(np.abs(self._characters[i])) phase = (np.angle(self._characters[i]) / np.pi * 180) % 360 if len(chars) > 1: w.write("[ [ %2d, %5.1f ]" % (chars[0], phase[0])) for chi, theta in zip(chars[1:], phase[1:]): w.write(", [ %2d, %5.1f ]" % (chi, theta)) w.write(" ]\n") else: w.write("[ [ %2d, %5.1f ] ]\n" % (chars[0], phase[0])) if show_irreps: self._write_yaml_irreps(w) w.close() def _write_yaml_irreps(self, file_pointer): w = file_pointer if not self._irreps: self._irrep = self._get_irreps() w.write("\n") w.write("irreps:\n") for i, (deg_set, irrep_Rs) in enumerate(zip(self._degenerate_sets, self._irreps)): w.write("- # %d\n" % (i + 1)) for j, irrep_R in enumerate(irrep_Rs): if self._rotation_symbols: symbol = self._rotation_symbols[j] else: symbol = '' if len(deg_set) > 1: w.write(" - # %d %s\n" % (j + 1, symbol)) for k, v in enumerate(irrep_R): w.write(" - [ ") for x in v[:-1]: w.write("%10.7f, %10.7f, " % (x.real, x.imag)) w.write("%10.7f, %10.7f ] # (" % (v[-1].real, v[-1].imag)) w.write(("%5.0f" * len(v)) % tuple( (np.angle(v) / np.pi * 180) % 360)) w.write(")\n") else: x = irrep_R[0][0] w.write(" - [ [ %10.7f, %10.7f ] ] # (%3.0f) %d %s\n" % (x.real, x.imag, (np.angle(x) / np.pi * 180) % 360, j + 1, symbol)) pass
class GroupVelocity(object): r"""Class to calculate group velocities of phonons d omega ---- ------- = \ / omega d q \/q Gradient of omega in reciprocal space, which is calculated here by 1 d D(q) ------- <e(q,nu)|------|e(q,nu)> 2 omega d q Attributes ---------- group_velocity : ndarray Group velocities at q-points. shape=(q-points, 3), dtype='double', order='C' q_length : float Distance in reciprocal space used to calculate finite difference of dynamcial matrix. """ def __init__(self, dynamical_matrix, q_length=None, symmetry=None, frequency_factor_to_THz=VaspToTHz, cutoff_frequency=1e-4): """ dynamical_matrix : DynamicalMatrix or DynamicalMatrixNAC Dynamical matrix class instance. q_length : float This is used such as D(q + q_length) - D(q - q_length) for calculating finite difference of dynamical matrix. Default is None, which gives 1e-5. symmetry : Symmetry This is used to symmetrize group velocity at each q-points. Default is None, which means no symmetrization. frequency_factor_to_THz : float Unit conversion factor to convert to THz. Default is VaspToTHz. cutoff_frequency : float Group velocity is set zero if phonon frequency is below this value. """ self._dynmat = dynamical_matrix primitive = dynamical_matrix.get_primitive() self._reciprocal_lattice_inv = primitive.get_cell() self._reciprocal_lattice = np.linalg.inv(self._reciprocal_lattice_inv) self._q_length = q_length if self._dynmat.is_nac() and self._dynmat.get_nac_method() == 'gonze': if self._q_length is None: self._q_length = 1e-5 if self._q_length is None: self._ddm = DerivativeOfDynamicalMatrix(dynamical_matrix) else: self._ddm = None self._symmetry = symmetry self._factor = frequency_factor_to_THz self._cutoff_frequency = cutoff_frequency self._directions = np.array( [[1, 2, 3], [1, 0, 0], [0, 1, 0], [0, 0, 1]], dtype='double') self._directions[0] /= np.linalg.norm(self._directions[0]) self._q_points = None self._group_velocities = None self._perturbation = None def run(self, q_points, perturbation=None): """Group velocities are computed at q-points. q_points : Array-like List of q-points such as [[0, 0, 0], [0.1, 0.2, 0.3], ...]. perturbation : Array-like Direction in fractional coordinates of reciprocal space. """ self._q_points = q_points self._perturbation = perturbation if perturbation is None: # Give an random direction to break symmetry self._directions[0] = np.array([1, 2, 3]) else: self._directions[0] = np.dot(self._reciprocal_lattice, perturbation) self._directions[0] /= np.linalg.norm(self._directions[0]) gv = [self._calculate_group_velocity_at_q(q) for q in self._q_points] self._group_velocities = np.array(gv, dtype='double', order='C') @property def q_length(self): return self._q_length def get_q_length(self): return self.q_length @q_length.setter def q_length(self, q_length): self._q_length = q_length def set_q_length(self, q_length): self.q_length = q_length @property def group_velocities(self): return self._group_velocities def get_group_velocity(self): return self.group_velocities def _calculate_group_velocity_at_q(self, q): self._dynmat.set_dynamical_matrix(q) dm = self._dynmat.get_dynamical_matrix() eigvals, eigvecs = np.linalg.eigh(dm) eigvals = eigvals.real freqs = np.sqrt(abs(eigvals)) * np.sign(eigvals) * self._factor gv = np.zeros((len(freqs), 3), dtype='double', order='C') deg_sets = degenerate_sets(freqs) ddms = self._get_dD(np.array(q)) pos = 0 for deg in deg_sets: gv[pos:pos + len(deg)] = self._perturb_D(ddms, eigvecs[:, deg]) pos += len(deg) for i, f in enumerate(freqs): if f > self._cutoff_frequency: gv[i, :] *= self._factor**2 / f / 2 else: gv[i, :] = 0 if self._perturbation is None: if self._symmetry is None: return gv else: return self._symmetrize_group_velocity(gv, q) else: return gv def _symmetrize_group_velocity(self, gv, q): """Symmetrize obtained group velocities using site symmetries.""" rotations = [] for r in self._symmetry.get_reciprocal_operations(): q_in_BZ = q - np.rint(q) diff = q_in_BZ - np.dot(r, q_in_BZ) if (np.abs(diff) < self._symmetry.get_symmetry_tolerance()).all(): rotations.append(r) gv_sym = np.zeros_like(gv) for r in rotations: r_cart = similarity_transformation(self._reciprocal_lattice, r) gv_sym += np.dot(r_cart, gv.T).T return gv_sym / len(rotations) def _get_dD(self, q): """Compute derivative or finite difference of dynamcial matrices""" if self._q_length is None: return self._get_dD_analytical(q) else: return self._get_dD_FD(q) def _get_dD_FD(self, q): """Compute finite difference of dynamcial matrices""" ddm = [] for dqc in self._directions * self._q_length: dq = np.dot(self._reciprocal_lattice_inv, dqc) ddm.append( delta_dynamical_matrix(q, dq, self._dynmat) / self._q_length / 2) return np.array(ddm) def _get_dD_analytical(self, q): """Compute derivative of dynamcial matrices""" self._ddm.run(q) ddm = self._ddm.get_derivative_of_dynamical_matrix() dtype = "c%d" % (np.dtype('double').itemsize * 2) ddm_dirs = np.zeros((len(self._directions), ) + ddm.shape[1:], dtype=dtype) for i, dq in enumerate(self._directions): for j in range(3): ddm_dirs[i] += dq[j] * ddm[j] return ddm_dirs def _perturb_D(self, ddms, eigsets): """Treat degeneracy Group velocities are calculated using analytical continuation using specified directions (self._directions) in reciprocal space. ddms : Array-like List of delta (derivative or finite difference) of dynamical matrices along several q-directions for perturbation. eigsets : Array-like List of phonon eigenvectors of degenerate bands. """ eigvals, eigvecs = np.linalg.eigh( np.dot(eigsets.T.conj(), np.dot(ddms[0], eigsets))) gv = [] rot_eigsets = np.dot(eigsets, eigvecs) for ddm in ddms[1:]: gv.append( np.diag(np.dot(rot_eigsets.T.conj(), np.dot(ddm, rot_eigsets))).real) return np.transpose(gv)
class GroupVelocity: """ d omega ---- ------- = \ / omega d q \/q Gradient of omega in reciprocal space. d D(q) <e(q,nu)|------|e(q,nu)> d q """ def __init__(self, dynamical_matrix, q_length=None, symmetry=None, frequency_factor_to_THz=VaspToTHz, cutoff_frequency=1e-4): """ q_points is a list of sets of q-point and q-direction: [[q-point, q-direction], [q-point, q-direction], ...] q_length is used such as D(q + q_length) - D(q - q_length). """ self._dynmat = dynamical_matrix primitive = dynamical_matrix.get_primitive() self._reciprocal_lattice_inv = primitive.get_cell() self._reciprocal_lattice = np.linalg.inv(self._reciprocal_lattice_inv) self._q_length = q_length if q_length is None: self._ddm = DerivativeOfDynamicalMatrix(dynamical_matrix) else: self._ddm = None self._symmetry = symmetry self._factor = frequency_factor_to_THz self._cutoff_frequency = cutoff_frequency self._directions = np.array( [[1, 2, 3], [1, 0, 0], [0, 1, 0], [0, 0, 1]], dtype='double') self._directions[0] /= np.linalg.norm(self._directions[0]) self._q_points = None self._group_velocity = None self._perturbation = None def set_q_points(self, q_points, perturbation=None): self._q_points = q_points self._perturbation = perturbation if perturbation is None: self._directions[0] = np.array([1, 2, 3]) else: self._directions[0] = np.dot(self._reciprocal_lattice, perturbation) self._directions[0] /= np.linalg.norm(self._directions[0]) self._set_group_velocity() def set_q_length(self, q_length): self._q_length = q_length def get_group_velocity(self): return self._group_velocity def _set_group_velocity(self): gv = [self._set_group_velocity_at_q(q) for q in self._q_points] self._group_velocity = np.array(gv) def _set_group_velocity_at_q(self, q): self._dynmat.set_dynamical_matrix(q) dm = self._dynmat.get_dynamical_matrix() eigvals, eigvecs = np.linalg.eigh(dm) eigvals = eigvals.real freqs = np.sqrt(abs(eigvals)) * np.sign(eigvals) * self._factor gv = np.zeros((len(freqs), 3), dtype='double') deg_sets = degenerate_sets(freqs) ddms = self._get_dD(np.array(q)) pos = 0 for deg in deg_sets: gv[pos:pos + len(deg)] = self._perturb_D(ddms, eigvecs[:, deg]) pos += len(deg) for i, f in enumerate(freqs): if f > self._cutoff_frequency: gv[i, :] *= self._factor**2 / f / 2 else: gv[i, :] = 0 if self._perturbation is None: return self._symmetrize_group_velocity(gv, q) else: return gv def _symmetrize_group_velocity(self, gv, q): rotations = [] for r in self._symmetry.get_reciprocal_operations(): q_in_BZ = q - np.rint(q) diff = q_in_BZ - np.dot(r, q_in_BZ) if (np.abs(diff) < self._symmetry.get_symmetry_tolerance()).all(): rotations.append(r) gv_sym = np.zeros_like(gv) for r in rotations: r_cart = similarity_transformation(self._reciprocal_lattice, r) gv_sym += np.dot(r_cart, gv.T).T return gv_sym / len(rotations) def _get_dD(self, q): if self._q_length is None: return self._get_dD_analytical(q) else: return self._get_dD_FD(q) def _get_dD_FD(self, q): # finite difference ddm = [] for dqc in self._directions * self._q_length: dq = np.dot(self._reciprocal_lattice_inv, dqc) ddm.append( delta_dynamical_matrix(q, dq, self._dynmat) / self._q_length / 2) return np.array(ddm) def _get_dD_analytical(self, q): self._ddm.run(q) ddm = self._ddm.get_derivative_of_dynamical_matrix() ddm_dirs = np.zeros((len(self._directions), ) + ddm.shape[1:], dtype='complex128') for i, dq in enumerate(self._directions): for j in range(3): ddm_dirs[i] += dq[j] * ddm[j] return ddm_dirs def _perturb_D(self, ddms, eigsets): eigvals, eigvecs = np.linalg.eigh( np.dot(eigsets.T.conj(), np.dot(ddms[0], eigsets))) gv = [] rot_eigsets = np.dot(eigsets, eigvecs) for ddm in ddms[1:]: gv.append( np.diag(np.dot(rot_eigsets.T.conj(), np.dot(ddm, rot_eigsets))).real) return np.transpose(gv)
class Modulation: def __init__(self, dynamical_matrix, cell, dimension, phonon_modes, delta_q=None, derivative_order=None, nac_q_direction=None, factor=VaspToTHz): """Class describe atomic modulations Atomic modulations corresponding to phonon modes are created. """ self._dm = dynamical_matrix self._cell = cell self._phonon_modes = phonon_modes self._dimension = dimension self._delta_q = delta_q # 1st/2nd order perturbation direction self._nac_q_direction = nac_q_direction self._ddm = DerivativeOfDynamicalMatrix(dynamical_matrix) self._derivative_order = derivative_order self._factor = factor self._delta_modulations = [] self._eigvecs = [] self._eigvals = [] def run(self): for ph_mode in self._phonon_modes: q, band_index, amplitude, argument = ph_mode eigvals, eigvecs = self._get_eigenvectors(q) u = self._get_delta(eigvecs[:, band_index], q) self._eigvecs.append(eigvecs[:, band_index]) self._eigvals.append(eigvals[band_index]) # Set phase of modulation so that phase of the element # that has maximum absolute value becomes 0. self._set_phase_of_modulation(u, argument) self._delta_modulations.append(u.reshape(-1,3) * amplitude) def write(self, filename="MPOSCAR"): deltas = [] for i, delta_positions in enumerate(self._delta_modulations): cell = self._get_cell_with_modulation(delta_positions) write_vasp((filename+"-%03d") % (i+1), cell, direct=True) deltas.append(delta_positions) sum_of_deltas = np.sum(deltas, axis=0) cell = self._get_cell_with_modulation(sum_of_deltas) write_vasp(filename, cell, direct=True) no_modulations = np.zeros(sum_of_deltas.shape, dtype=complex) cell = self._get_cell_with_modulation(no_modulations) write_vasp(filename+"-orig", cell, direct=True) def get_modulations(self): modulations = [] for delta_positions in self._delta_modulations: modulations.append(self._get_cell_with_modulation(delta_positions)) return modulations def get_delta_modulations(self): return self._delta_modulations, self._get_supercell() def _get_cell_with_modulation(self, modulation): supercell = self._get_supercell() lattice = supercell.get_cell() positions = supercell.get_positions() positions += modulation.real scaled_positions = np.dot(positions, np.linalg.inv(lattice)) for p in scaled_positions: p -= np.floor(p) supercell.set_scaled_positions(scaled_positions) return supercell def _get_delta(self, eigvec, q): dim = self._dimension m = self._cell.get_masses() r = self._cell.get_scaled_positions() u = [] for a, b, c in list(np.ndindex(tuple(dim))): for i, e in enumerate(eigvec): phase = 2j * np.pi * ( np.dot(r[i//3] + np.array([a,b,c]), q)) u.append(e / np.sqrt(m[i//3]) * np.exp(phase)) return np.array(u) def _set_phase_of_modulation(self, modulation, argument): u = modulation index_max_elem = np.argmax(abs(u)) max_elem = u[index_max_elem] phase_for_zero = max_elem / abs(max_elem) phase_factor = np.exp(1j * np.pi * argument / 180) / phase_for_zero u *= phase_factor def _get_supercell(self): dim = self._dimension scaled_positions = [] masses = [] magmoms_prim = self._cell.get_magnetic_moments() if magmoms_prim == None: magmoms = None else: magmoms = [] symbols = [] for a in range(dim[0]): for b in range(dim[1]): for c in range(dim[2]): for i in range(self._cell.get_number_of_atoms()): p = self._cell.get_scaled_positions()[i] scaled_positions.append(p + np.array([a,b,c])) masses.append(self._cell.get_masses()[i]) symbols.append(self._cell.get_chemical_symbols()[i]) if not magmoms_prim == None: magmoms.append(magmoms_prim[i]) lattice = np.dot(np.diag(dim), self._cell.get_cell()) positions = np.dot(scaled_positions, self._cell.get_cell()) return Atoms(cell=lattice, positions=positions, masses=masses, magmoms=magmoms, symbols=symbols, pbc=True) def _get_eigenvectors(self, q): if self._nac_q_direction is not None and (np.abs(q) < 1e-5).all(): self._dm.set_dynamical_matrix(q, q_direction=self._nac_q_direction) else: self._dm.set_dynamical_matrix(q) eigvals, eigvecs = np.linalg.eigh(self._dm.get_dynamical_matrix()) eigvals = eigvals.real if self._delta_q is None: return eigvals, eigvecs eigvecs_new = np.zeros_like(eigvecs) deg_sets = degenerate_sets(eigvals) if self._derivative_order is None: self._ddm.set_derivative_order(1) dD1 = self._get_dD(q) self._ddm.set_derivative_order(2) dD2 = self._get_dD(q) else: self._ddm.set_derivative_order(self._derivative_order) dD = self._get_dD(q) for deg in deg_sets: if len(deg) == 1: continue if self._derivative_order is not None: eigvecs_new = self._rearrange_eigenvectors(dD, eigvecs[:, deg]) else: eigvecs_new = self._rearrange_eigenvectors(dD1, eigvecs[:, deg]) if eigvecs_new is None: eigvecs_new = self._rearrange_eigenvectors( dD2, eigvecs[:, deg]) if eigvecs_new is not None: eigvecs[:, deg] = eigvecs_new return eigvals, eigvecs def _check_eigvecs(self, eigvals, eigvecs, dynmat): modified = np.diag(np.dot(eigvecs.conj().T, np.dot(dynmat, eigvecs))) print self._eigvals_to_frequencies(eigvals) print self._eigvals_to_frequencies(modified) print def _eigvals_to_frequencies(self, eigvals): e = np.array(eigvals).real return np.sqrt(np.abs(e)) * np.sign(e) * self._factor def _get_dD(self, q): self._ddm.run(q) ddm = self._ddm.get_derivative_of_dynamical_matrix() dD = np.zeros(ddm.shape[1:], dtype='complex128') for i in range(3): dD += self._delta_q[i] * ddm[i] return dD / np.linalg.norm(self._delta_q) def _rearrange_eigenvectors(self, dD, eigvecs_deg): dD_part = np.dot(eigvecs_deg.T.conj(), np.dot(dD, eigvecs_deg)) p_eigvals, p_eigvecs = np.linalg.eigh(dD_part) if (np.abs(p_eigvals - p_eigvals[0]) < 1e-5).all(): return None else: return np.dot(eigvecs_deg, p_eigvecs) def write_yaml(self): file = open('modulation.yaml', 'w') dim = self._dimension factor = self._factor cell = self._cell num_atom = self._cell.get_number_of_atoms() modes = self._phonon_modes lattice = cell.get_cell() positions = cell.get_scaled_positions() masses = cell.get_masses() symbols = cell.get_chemical_symbols() file.write("unitcell:\n") file.write(" atom-info:\n") for m, s in zip( masses, symbols ): file.write(" - { name: %2s, mass: %10.5f }\n" % (s, m)) file.write(" lattice-vectors:\n") file.write(" - [ %20.15f, %20.15f, %20.15f ]\n" % (tuple(lattice[0]))) file.write(" - [ %20.15f, %20.15f, %20.15f ]\n" % (tuple(lattice[1]))) file.write(" - [ %20.15f, %20.15f, %20.15f ]\n" % (tuple(lattice[2]))) file.write(" positions:\n") for p in positions: file.write(" - [ %20.15f, %20.15f, %20.15f ]\n" % (tuple(p))) supercell = self._get_supercell() lattice = supercell.get_cell() positions = supercell.get_scaled_positions() masses = supercell.get_masses() symbols = supercell.get_chemical_symbols() file.write("supercell:\n") file.write(" dimension: [ %d, %d, %d ]\n" % tuple(dim)) file.write(" atom-info:\n") for m, s in zip( masses, symbols ): file.write(" - { name: %2s, mass: %10.5f }\n" % (s, m)) file.write(" lattice-vectors:\n") file.write(" - [ %20.15f, %20.15f, %20.15f ]\n" % (tuple(lattice[0]))) file.write(" - [ %20.15f, %20.15f, %20.15f ]\n" % (tuple(lattice[1]))) file.write(" - [ %20.15f, %20.15f, %20.15f ]\n" % (tuple(lattice[2]))) file.write(" positions:\n") for p in positions: file.write(" - [ %20.15f, %20.15f, %20.15f ]\n" % (tuple(p))) file.write("modulations:\n") for deltas, mode in zip(self._delta_modulations, self._phonon_modes): q = mode[0] file.write("- q-position: [ %12.7f, %12.7f, %12.7f ]\n" % tuple(q)) file.write(" band: %d\n" % (mode[1] + 1)) file.write(" amplitude: %f\n" % mode[2]) file.write(" phase: %f\n" % mode[3]) file.write(" displacements:\n") for i, p in enumerate(deltas): file.write(" - [ %20.15f, %20.15f ] # %d x (%f)\n" % (p[0].real, p[0].imag, i + 1, abs(p[0]))) file.write(" - [ %20.15f, %20.15f ] # %d y (%f)\n" % (p[1].real, p[1].imag, i + 1, abs(p[1]))) file.write(" - [ %20.15f, %20.15f ] # %d z (%f)\n" % (p[2].real, p[2].imag, i + 1, abs(p[2]))) file.write("phonon:\n") freqs = self._eigvals_to_frequencies(self._eigvals) for eigvec, freq, mode in zip(self._eigvecs, freqs, self._phonon_modes): file.write("- q-position: [ %12.7f, %12.7f, %12.7f ]\n" % tuple(mode[0])) file.write(" band: %d\n" % (mode[1] + 1)) file.write(" amplitude: %f\n" % mode[2]) file.write(" phase: %f\n" % mode[3]) file.write(" frequency: %15.10f\n" % freq) file.write(" eigenvector:\n") for j in range(num_atom): file.write(" - # atom %d\n" % (j + 1)) for k in (0, 1, 2): val = eigvec[j * 3 + k] file.write(" - [ %17.14f, %17.14f ] # %f\n" % (val.real, val.imag, np.angle(val, deg=True)))
class IrReps: """Class to calculate irreducible representations from eigenvectors. Methods and terminologies used in this class may be easily found in textbooks such as - Group theory with applications in chemical physics by Patrick Jacobs - Symmetry and condensed matter physics by M. El-Batanouny and F. Wooten """ def __init__( self, dynamical_matrix: Union[DynamicalMatrix, DynamicalMatrixNAC], q, is_little_cogroup=False, nac_q_direction=None, factor=VaspToTHz, symprec=1e-5, degeneracy_tolerance=None, log_level=0, ): """Init method.""" self._is_little_cogroup = is_little_cogroup self._nac_q_direction = nac_q_direction self._factor = factor self._log_level = log_level self._q = np.array(q) if degeneracy_tolerance is None: self._degeneracy_tolerance = 1e-5 else: self._degeneracy_tolerance = degeneracy_tolerance self._symprec = symprec self._primitive = dynamical_matrix.primitive self._dynamical_matrix = dynamical_matrix self._ddm = DerivativeOfDynamicalMatrix(dynamical_matrix) self._character_table = None self._symmetry_dataset = Symmetry( self._primitive, symprec=self._symprec ).dataset if not is_primitive_cell(self._symmetry_dataset["rotations"]): raise RuntimeError( "Non-primitve cell is used. Your unit cell may be transformed to " "a primitive cell by PRIMITIVE_AXIS tag." ) def run(self): """Calculate irreps.""" self._set_eigenvectors(self._dynamical_matrix) (self._rotations_at_q, self._translations_at_q) = self._get_rotations_at_q() self._g = len(self._rotations_at_q) ( self._pointgroup_symbol, self._transformation_matrix, self._conventional_rotations, ) = self._get_conventional_rotations() self._ground_matrices = self._get_ground_matrix() self._degenerate_sets = self._get_degenerate_sets() self._irreps = self._get_irreps() self._characters, self._irrep_dims = self._get_characters() self._ir_labels = None if ( self._pointgroup_symbol in character_table.keys() and character_table[self._pointgroup_symbol] is not None ): self._rotation_symbols = self._get_rotation_symbols() if (abs(self._q) < self._symprec).all() and self._rotation_symbols: self._ir_labels = self._get_ir_labels() elif (abs(self._q) < self._symprec).all(): if self._log_level > 0: print("Database for this point group is not preprared.") else: if self._log_level > 0: print("Database for non-Gamma point is not prepared.") else: self._rotation_symbols = None return True def _get_degenerate_sets(self): deg_sets = get_degenerate_sets(self._freqs, cutoff=self._degeneracy_tolerance) self._ddm.run(self._q) return deg_sets def get_band_indices(self): """Return band indices. Returns ------- See docstring of ``degenerate_sets``. """ return self._degenerate_sets def get_characters(self): """Return characters of irreps.""" return self._characters def get_eigenvectors(self): """Return eigenvectors.""" return self._eigvecs def get_irreps(self): """Return irreps.""" return self._irreps def get_ground_matrices(self): """Return ground matrices.""" return self._ground_matrices def get_rotation_symbols(self): """Return symbols assigned to rotation matrices.""" return self._rotation_symbols def get_rotations(self): """Return rotation matrices.""" return self._conventional_rotations def get_projection_operators(self, idx_irrep, i=None, j=None): """Return projection operators.""" if i is None or j is None: return self._get_character_projection_operators(idx_irrep) else: return self._get_projection_operators(idx_irrep, i, j) def show(self, show_irreps=False): """Show irreps.""" self._show(show_irreps) def write_yaml(self, show_irreps=False): """Write irreps in yaml file.""" self._write_yaml(show_irreps) def _set_eigenvectors(self, dm): if self._nac_q_direction is not None and (np.abs(self._q) < 1e-5).all(): dm.run(self._q, q_direction=self._nac_q_direction) else: dm.run(self._q) eigvals, self._eigvecs = np.linalg.eigh(dm.dynamical_matrix) self._freqs = np.sqrt(abs(eigvals)) * np.sign(eigvals) * self._factor def _get_rotations_at_q(self): rotations_at_q = [] trans_at_q = [] for r, t in zip( self._symmetry_dataset["rotations"], self._symmetry_dataset["translations"] ): # Using r is used instead of np.linalg.inv(r) diff = np.dot(self._q, r) - self._q if (abs(diff - np.rint(diff)) < self._symprec).all(): rotations_at_q.append(r) for i in range(3): if np.abs(t[i] - 1) < self._symprec: t[i] = 0.0 trans_at_q.append(t) return np.array(rotations_at_q), np.array(trans_at_q) def _get_conventional_rotations(self): rotations = self._rotations_at_q.copy() pointgroup_symbol = self._symmetry_dataset["pointgroup"] transformation_matrix = self._symmetry_dataset["transformation_matrix"] conventional_rotations = self._transform_rotations( transformation_matrix, rotations ) return (pointgroup_symbol, transformation_matrix, conventional_rotations) def _transform_rotations(self, tmat, rotations): trans_rots = [] for r in rotations: r_conv = similarity_transformation(tmat, r) trans_rots.append(np.rint(r_conv).astype(int)) return np.array(trans_rots) def _get_ground_matrix(self): matrices = [] for (r, t) in zip(self._rotations_at_q, self._translations_at_q): lat = self._primitive.cell.T r_cart = similarity_transformation(lat, r) perm_mat = self._get_modified_permutation_matrix(r, t) matrices.append(np.kron(perm_mat, r_cart)) return np.array(matrices) def _get_characters(self): characters = [] irrep_dims = [] for irrep_Rs in self._irreps: characters.append([np.trace(rep) for rep in irrep_Rs]) irrep_dims.append(len(irrep_Rs[0])) return np.array(characters), np.array(irrep_dims) def _get_modified_permutation_matrix(self, r, t): num_atom = len(self._primitive) pos = self._primitive.scaled_positions matrix = np.zeros((num_atom, num_atom), dtype=complex) for i, p1 in enumerate(pos): p_rot = np.dot(r, p1) + t for j, p2 in enumerate(pos): diff = p_rot - p2 if (abs(diff - np.rint(diff)) < self._symprec).all(): # For this phase factor, see # Dynamics of perfect crystals by G. Venkataraman et al., # pp132 Eq. (3.22). # It is assumed that dynamical matrix is built without # considering internal atomic positions, so # the phase factors of eigenvectors are shifted in # _get_irreps(). phase_factor = np.dot(self._q, np.dot(np.linalg.inv(r), p2 - p_rot)) # This phase factor comes from non-pure-translation of # each symmetry opration. if self._is_little_cogroup: phase_factor += np.dot(t, self._q) matrix[j, i] = np.exp(2j * np.pi * phase_factor) return matrix def _get_irreps(self): eigvecs = [] phases = np.kron( [ np.exp(2j * np.pi * np.dot(self._q, pos)) for pos in self._primitive.scaled_positions ], [1, 1, 1], ) for vec in self._eigvecs.T: eigvecs.append(vec * phases) irrep = [] for band_indices in self._degenerate_sets: irrep_Rs = [] for mat in self._ground_matrices: n_deg = len(band_indices) if n_deg == 1: vec = eigvecs[band_indices[0]] irrep_Rs.append([[np.vdot(vec, np.dot(mat, vec))]]) continue irrep_R = np.zeros((n_deg, n_deg), dtype=complex) for i, b_i in enumerate(band_indices): vec_i = eigvecs[b_i] for j, b_j in enumerate(band_indices): vec_j = eigvecs[b_j] irrep_R[i, j] = np.vdot(vec_i, np.dot(mat, vec_j)) irrep_Rs.append(irrep_R) irrep.append(irrep_Rs) return irrep def _get_character_projection_operators(self, idx_irrep): dim = self._irrep_dims[idx_irrep] chars = self._characters[idx_irrep] return ( np.sum( [mat * char.conj() for mat, char in zip(self._ground_matrices, chars)], axis=0, ) * dim / self._g ) def _get_projection_operators(self, idx_irrep, i, j): dim = self._irrep_dims[idx_irrep] return ( np.sum( [ mat * r[i, j].conj() for mat, r in zip(self._ground_matrices, self._irreps[idx_irrep]) ], axis=0, ) * dim / self._g ) def _get_rotation_symbols(self): ptg_symbol = self._pointgroup_symbol for ct in character_table[ptg_symbol]: mapping_table = ct["mapping_table"] rotation_symbols = [] for r in self._conventional_rotations: rotation_symbols.append(_get_rotation_symbol(r, mapping_table)) if False in rotation_symbols: ret_val = None else: ret_val = rotation_symbols if ret_val is not None: self._character_table = ct break return ret_val def _get_ir_labels(self): ir_labels = [] rot_list = self._character_table["rotation_list"] char_table = self._character_table["character_table"] for chars, deg_set in zip(self._characters, self._degenerate_sets): chars_ordered = np.zeros(len(rot_list), dtype=complex) for rs, ch in zip(self._rotation_symbols, chars): chars_ordered[rot_list.index(rs)] += ch for i, rl in enumerate(rot_list): chars_ordered[i] /= len(self._character_table["mapping_table"][rl]) found = False for ct_label in char_table.keys(): if ( abs(chars_ordered - np.array(char_table[ct_label])) < self._symprec ).all(): ir_labels.append(ct_label) found = True break if not found: ir_labels.append(None) if self._log_level > 1: text = "" for v in chars_ordered: text += "%5.2f " % abs(v) if found: print("%s %s" % (text, ct_label)) else: print("%s Not found" % text) return ir_labels def _show(self, show_irreps): print("") print("-------------------------------") print(" Irreducible representations") print("-------------------------------") print("q-point: %s" % self._q) print("Point group: %s" % self._pointgroup_symbol) print("") if (np.abs(self._q) < self._symprec).all(): width = 6 print("Original rotation matrices:") print("") _print_rotations(self._rotations_at_q, width=width) else: width = 4 print("Original symmetry operations:") print("") _print_rotations( self._rotations_at_q, translations=self._translations_at_q, width=width ) print("Transformation matrix:") print("") for v in self._transformation_matrix: print("%6.3f %6.3f %6.3f" % tuple(v)) print("") print("Rotation matrices by transformation matrix:") print("") _print_rotations( self._conventional_rotations, rotation_symbols=self._rotation_symbols, width=width, ) print("Character table:") print("") for i, deg_set in enumerate(self._degenerate_sets): text = "%3d (%8.3f): " % (deg_set[0] + 1, self._freqs[deg_set[0]]) if self._ir_labels is None: print(text) elif self._ir_labels[i] is None: warning = "Not found. Try adjusting tolerance value in IRREPS." print("%s%s" % (text, warning)) else: print("%s%s" % (text, self._ir_labels[i])) _print_characters(self._characters[i]) print("") if show_irreps: self._show_irreps() def _show_irreps(self): print("IR representations:") print("") for i, (deg_set, irrep_Rs) in enumerate( zip(self._degenerate_sets, self._irreps) ): print("%3d (%8.3f):" % (deg_set[0] + 1, self._freqs[deg_set[0]])) print("") for j, irrep_R in enumerate(irrep_Rs): for k, irrep_Rk in enumerate(irrep_R): text = " " for ll, irrep_Rkl in enumerate(irrep_Rk): if irrep_Rkl.real > 0: sign_r = " " else: sign_r = "-" if irrep_Rkl.imag > 0: sign_i = "+" else: sign_i = "-" if k == 0: str_index = "%2d" % (j + 1) else: str_index = " " if ll > 0: str_index = "" text += "%s (%s%5.3f %s%5.3fi) " % ( str_index, sign_r, abs(irrep_Rkl.real), sign_i, abs(irrep_Rkl.imag), ) print(text) if len(irrep_R) > 1: print("") if len(irrep_R) == 1: print("") def _write_yaml(self, show_irreps): w = open("irreps.yaml", "w") w.write("q-position: [ %12.7f, %12.7f, %12.7f ]\n" % tuple(self._q)) w.write("point_group: %s\n" % self._pointgroup_symbol) w.write("transformation_matrix:\n") for v in self._transformation_matrix: w.write("- [ %10.7f, %10.7f, %10.7f ]\n" % tuple(v)) w.write("rotations:\n") for i, r in enumerate(self._conventional_rotations): w.write("- matrix:\n") for v in r: w.write(" - [ %2d, %2d, %2d ]\n" % tuple(v)) if self._rotation_symbols: w.write(" symbol: %s\n" % self._rotation_symbols[i]) w.write("normal_modes:\n") for i, deg_set in enumerate(self._degenerate_sets): w.write("- band_indices: [ ") w.write("%d" % (deg_set[0] + 1)) for bi in deg_set[1:]: w.write(", %d" % (bi + 1)) w.write(" ]\n") w.write(" frequency: %-15.10f\n" % self._freqs[deg_set[0]]) if self._ir_labels: w.write(" ir_label: %s\n" % self._ir_labels[i]) w.write(" characters: ") chars = np.rint(np.abs(self._characters[i])) phase = (np.angle(self._characters[i]) / np.pi * 180) % 360 if len(chars) > 1: w.write("[ [ %2d, %5.1f ]" % (chars[0], phase[0])) for chi, theta in zip(chars[1:], phase[1:]): w.write(", [ %2d, %5.1f ]" % (chi, theta)) w.write(" ]\n") else: w.write("[ [ %2d, %5.1f ] ]\n" % (chars[0], phase[0])) if show_irreps: self._write_yaml_irreps(w) w.close() def _write_yaml_irreps(self, file_pointer): w = file_pointer if not self._irreps: self._irrep = self._get_irreps() w.write("\n") w.write("irreps:\n") for i, (deg_set, irrep_Rs) in enumerate( zip(self._degenerate_sets, self._irreps) ): w.write("- # %d\n" % (i + 1)) for j, irrep_R in enumerate(irrep_Rs): if self._rotation_symbols: symbol = self._rotation_symbols[j] else: symbol = "" if len(deg_set) > 1: w.write(" - # %d %s\n" % (j + 1, symbol)) for k, v in enumerate(irrep_R): w.write(" - [ ") for x in v[:-1]: w.write("%10.7f, %10.7f, " % (x.real, x.imag)) w.write("%10.7f, %10.7f ] # (" % (v[-1].real, v[-1].imag)) w.write( ("%5.0f" * len(v)) % tuple((np.angle(v) / np.pi * 180) % 360) ) w.write(")\n") else: x = irrep_R[0][0] w.write( " - [ [ %10.7f, %10.7f ] ] # (%3.0f) %d %s\n" % ( x.real, x.imag, (np.angle(x) / np.pi * 180) % 360, j + 1, symbol, ) ) pass
class IrReps: def __init__(self, dynamical_matrix, q, is_little_cogroup=False, nac_q_direction=None, factor=VaspToTHz, symprec=1e-5, degeneracy_tolerance=1e-5, log_level=0): self._is_little_cogroup = is_little_cogroup self._nac_q_direction = nac_q_direction self._factor = factor self._log_level = log_level self._q = np.array(q) self._degeneracy_tolerance = degeneracy_tolerance self._symprec = symprec self._primitive = dynamical_matrix.get_primitive() self._dynamical_matrix = dynamical_matrix self._ddm = DerivativeOfDynamicalMatrix(dynamical_matrix) def run(self): self._set_eigenvectors(self._dynamical_matrix) self._symmetry_dataset = Symmetry(self._primitive, self._symprec).get_dataset() if not self._is_primitive_cell(): print('') print("Non-primitve cell is used.") print("Your unit cell may be transformed to a primitive cell " "by PRIMITIVE_AXIS tag.") return False (self._rotations_at_q, self._translations_at_q) = self._get_rotations_at_q() self._g = len(self._rotations_at_q) (self._pointgroup_symbol, self._transformation_matrix, self._conventional_rotations) = self._get_conventional_rotations() self._ground_matrices = self._get_ground_matrix() self._degenerate_sets = self._get_degenerate_sets() self._irreps = self._get_irreps() self._characters, self._irrep_dims = self._get_characters() self._ir_labels = None if self._pointgroup_symbol in character_table.keys(): self._rotation_symbols = self._get_rotation_symbols() if (abs(self._q) < self._symprec).all() and self._rotation_symbols: self._ir_labels = self._get_ir_labels() elif (abs(self._q) < self._symprec).all(): if self._log_level > 0: print("Database for this point group is not preprared.") else: if self._log_level > 0: print("Database for non-Gamma point is not prepared.") else: self._rotation_symbols = None return True def _get_degenerate_sets(self): deg_sets = get_degenerate_sets(self._freqs, cutoff=self._degeneracy_tolerance) self._ddm.run(self._q) ddm_q = np.sum([self._ddm.get_derivative_of_dynamical_matrix()[i] * self._q[i] for i in range(3)], axis=0) return deg_sets def get_band_indices(self): return self._degenerate_sets def get_characters(self): return self._characters def get_eigenvectors(self): return self._eigvecs def get_irreps(self): return self._irreps def get_ground_matrices(self): return self._ground_matrices def get_rotation_symbols(self): return self._rotation_symbols def get_rotations(self): return self._conventional_rotations def get_projection_operators(self, idx_irrep, i=None, j=None): if i is None or j is None: return self._get_character_projection_operators(idx_irrep) else: return self._get_projection_operators(idx_irrep, i, j) def show(self, show_irreps=False): self._show(show_irreps) def write_yaml(self, show_irreps=False): self._write_yaml(show_irreps) def _set_eigenvectors(self, dm): if self._nac_q_direction is not None and (np.abs(self._q) < 1e-5).all(): dm.set_dynamical_matrix(self._q, q_direction=self._nac_q_direction) else: dm.set_dynamical_matrix(self._q) eigvals, self._eigvecs = np.linalg.eigh(dm.get_dynamical_matrix()) self._freqs = np.sqrt(abs(eigvals)) * np.sign(eigvals) * self._factor def _get_rotations_at_q(self): rotations_at_q = [] trans_at_q = [] for r, t in zip(self._symmetry_dataset['rotations'], self._symmetry_dataset['translations']): # Using r is used instead of np.linalg.inv(r) diff = np.dot(self._q, r) - self._q if (abs(diff - np.rint(diff)) < self._symprec).all(): rotations_at_q.append(r) for i in range(3): if np.abs(t[i] - 1) < self._symprec: t[i] = 0.0 trans_at_q.append(t) return np.array(rotations_at_q), np.array(trans_at_q) def _get_conventional_rotations(self): spacegroup_symbol = self._symmetry_dataset['international'][0] spacegroup_number = self._symmetry_dataset['number'] rotations = self._rotations_at_q.copy() pointgroup = get_pointgroup(rotations) pointgroup_symbol = pointgroup[0] transformation_matrix = pointgroup[1] conventional_rotations = self._transform_rotations( transformation_matrix, rotations) return (pointgroup_symbol, transformation_matrix, conventional_rotations) def _transform_rotations(self, tmat, rotations): trans_rots = [] for r in rotations: r_conv = similarity_transformation(np.linalg.inv(tmat), r) trans_rots.append(np.rint(r_conv).astype(int)) return np.array(trans_rots) def _get_ground_matrix(self): matrices = [] for (r, t) in zip(self._rotations_at_q, self._translations_at_q): lat = self._primitive.get_cell().T r_cart = similarity_transformation(lat, r) perm_mat = self._get_modified_permutation_matrix(r, t) matrices.append(np.kron(perm_mat, r_cart)) return np.array(matrices) def _get_characters(self): characters = [] irrep_dims = [] for irrep_Rs in self._irreps: characters.append([np.trace(rep) for rep in irrep_Rs]) irrep_dims.append(len(irrep_Rs[0])) return np.array(characters), np.array(irrep_dims) def _get_modified_permutation_matrix(self, r, t, ): num_atom = self._primitive.get_number_of_atoms() pos = self._primitive.get_scaled_positions() matrix = np.zeros((num_atom, num_atom), dtype=complex) for i, p1 in enumerate(pos): p_rot = np.dot(r, p1) + t for j, p2 in enumerate(pos): diff = p_rot - p2 if (abs(diff - np.rint(diff)) < self._symprec).all(): # For this phase factor, see # Dynamics of perfect crystals by G. Venkataraman et al., # pp132 Eq. (3.22). # It is assumed that dynamical matrix is built without # considering internal atomic positions, so # the phase factors of eigenvectors are shifted in # _get_irreps(). phase_factor = np.dot( self._q, np.dot(np.linalg.inv(r), p2 - p_rot)) # This phase factor comes from non-pure-translation of # each symmetry opration. if self._is_little_cogroup: phase_factor += np.dot(t, self._q) matrix[j, i] = np.exp(2j * np.pi * phase_factor) return matrix def _get_irreps(self): eigvecs = [] phases = np.kron( [np.exp(2j * np.pi * np.dot(self._q, pos)) for pos in self._primitive.get_scaled_positions()], [1, 1, 1]) for vec in self._eigvecs.T: eigvecs.append(vec * phases) irrep = [] for band_indices in self._degenerate_sets: irrep_Rs = [] for mat in self._ground_matrices: l = len(band_indices) if l == 1: vec = eigvecs[band_indices[0]] irrep_Rs.append([[np.vdot(vec, np.dot(mat, vec))]]) continue irrep_R = np.zeros((l, l), dtype=complex) for i, b_i in enumerate(band_indices): vec_i = eigvecs[b_i] for j, b_j in enumerate(band_indices): vec_j = eigvecs[b_j] irrep_R[i, j] = np.vdot(vec_i, np.dot(mat, vec_j)) irrep_Rs.append(irrep_R) irrep.append(irrep_Rs) return irrep def _get_character_projection_operators(self, idx_irrep): dim = self._irrep_dims[idx_irrep] chars = self._characters[idx_irrep] return np.sum([mat * char.conj() for mat, char in zip(self._ground_matrices, chars)], axis=0) * dim / self._g def _get_projection_operators(self, idx_irrep, i, j): dim = self._irrep_dims[idx_irrep] return np.sum([mat * r[i, j].conj() for mat, r in zip(self._ground_matrices, self._irreps[idx_irrep])], axis=0) * dim / self._g def _get_rotation_symbols(self): ptg = self._pointgroup_symbol for mapping_table in character_table[ptg]['mapping_table']: rotation_symbols = [] for r in self._conventional_rotations: symbol = get_rotation_symbol(r, mapping_table) rotation_symbols.append(symbol) if not False in rotation_symbols: break if False in rotation_symbols: return None else: return rotation_symbols def _get_ir_labels(self): ir_labels = [] rot_list = character_table[self._pointgroup_symbol]['rotation_list'] char_table = character_table[self._pointgroup_symbol]['character_table'] for chars, deg_set in zip(self._characters, self._degenerate_sets): chars_ordered = np.zeros(len(rot_list), dtype=complex) for rs, ch in zip(self._rotation_symbols, chars): chars_ordered[rot_list.index(rs)] += ch for i, rl in enumerate(rot_list): chars_ordered[i] /= len( character_table[self._pointgroup_symbol]['mapping_table'][0][rl]) found = False for ct_label in char_table.keys(): if (abs(chars_ordered - np.array(char_table[ct_label])) < self._symprec).all(): ir_labels.append(ct_label) found = True break if not found: ir_labels.append(None) if self._log_level > 1: text = "" for v in chars_ordered: text += "%5.2f " % abs(v) if found: print("%s %s" % (text, ct_label)) else: print("%s Not found" % text) return ir_labels def _is_primitive_cell(self): num_identity = 0 for r in self._symmetry_dataset['rotations']: if (r - np.eye(3, dtype='intc') == 0).all(): num_identity += 1 if num_identity > 1: return False else: return True def _show(self, show_irreps): print('') print("-------------------------------") print(" Irreducible representations") print("-------------------------------") print("q-point: %s" % self._q) print("Point group: %s" % self._pointgroup_symbol) print('') if (np.abs(self._q) < self._symprec).all(): width = 6 print("Original rotation matrices:") print('') print_rotations(self._rotations_at_q, width=width) else: width = 4 print("Original symmetry operations:") print('') print_rotations(self._rotations_at_q, translations=self._translations_at_q, width=width) print("Transformation matrix:") print('') for v in self._transformation_matrix: print("%6.3f %6.3f %6.3f" % tuple(v)) print('') print("Rotation matrices by transformation matrix:") print('') print_rotations(self._conventional_rotations, rotation_symbols=self._rotation_symbols, width=width) print("Character table:") print('') for i, deg_set in enumerate(self._degenerate_sets): text = "%3d (%8.3f): " % (deg_set[0] + 1, self._freqs[deg_set[0]]) if self._ir_labels is None: print(text) else: print(text + self._ir_labels[i]) print_characters(self._characters[i]) print('') if show_irreps: self._show_irreps() def _show_irreps(self): print("IR representations:") print('') for i, (deg_set, irrep_Rs) in enumerate(zip(self._degenerate_sets, self._irreps)): print("%3d (%8.3f):" % (deg_set[0] + 1, self._freqs[deg_set[0]])) print('') for j, irrep_R in enumerate(irrep_Rs): for k in range(len(irrep_R)): text = " " for l in range(len(irrep_R)): if irrep_R[k][l].real > 0: sign_r = " " else: sign_r = "-" if irrep_R[k][l].imag > 0: sign_i = "+" else: sign_i = "-" if k == 0: str_index = "%2d" % (j + 1) else: str_index = " " if l > 0: str_index = '' text += "%s (%s%5.3f %s%5.3fi) " % ( str_index, sign_r, abs(irrep_R[k][l].real), sign_i, abs(irrep_R[k][l].imag)) print(text) if len(irrep_R) > 1: print('') if len(irrep_R) == 1: print('') def _write_yaml(self, show_irreps): w = open("irreps.yaml", 'w') w.write("q-position: [ %12.7f, %12.7f, %12.7f ]\n" % tuple(self._q)) w.write("point_group: %s\n" % self._pointgroup_symbol) w.write("transformation_matrix:\n") for v in self._transformation_matrix: w.write("- [ %10.7f, %10.7f, %10.7f ]\n" % tuple(v)) w.write("rotations:\n") for i, r in enumerate(self._conventional_rotations): w.write("- matrix:\n") for v in r: w.write(" - [ %2d, %2d, %2d ]\n" % tuple(v)) if self._rotation_symbols: w.write(" symbol: %s\n" % self._rotation_symbols[i]) w.write("normal_modes:\n") for i, deg_set in enumerate(self._degenerate_sets): w.write("- band_indices: [ ") w.write("%d" % (deg_set[0] + 1)) for bi in deg_set[1:]: w.write(", %d" % (bi + 1)) w.write(" ]\n") w.write(" frequency: %-15.10f\n" % self._freqs[deg_set[0]]) if self._ir_labels: w.write(" ir_label: %s\n" % self._ir_labels[i]) w.write(" characters: ") chars = np.rint(np.abs(self._characters[i])) phase = (np.angle(self._characters[i]) / np.pi * 180) % 360 if len(chars) > 1: w.write("[ [ %2d, %5.1f ]" % (chars[0], phase[0])) for chi, theta in zip(chars[1:], phase[1:]): w.write(", [ %2d, %5.1f ]" % (chi, theta)) w.write(" ]\n") else: w.write("[ [ %2d, %5.1f ] ]\n" % (chars[0], phase[0])) if show_irreps: self._write_yaml_irreps(w) w.close() def _write_yaml_irreps(self, file_pointer): w = file_pointer if not self._irreps: self._irrep = self._get_irreps() w.write("\n") w.write("irreps:\n") for i, (deg_set, irrep_Rs) in enumerate( zip(self._degenerate_sets, self._irreps)): w.write("- # %d\n" % (i + 1)) for j, irrep_R in enumerate(irrep_Rs): if self._rotation_symbols: symbol = self._rotation_symbols[j] else: symbol = '' if len(deg_set) > 1: w.write(" - # %d %s\n" % (j + 1, symbol)) for k, v in enumerate(irrep_R): w.write(" - [ ") for x in v[:-1]: w.write("%10.7f, %10.7f, " % (x.real, x.imag)) w.write("%10.7f, %10.7f ] # (" % (v[-1].real, v[-1].imag)) w.write(("%5.0f" * len(v)) % tuple((np.angle(v) / np.pi * 180) % 360)) w.write(")\n") else: x = irrep_R[0][0] w.write(" - [ [ %10.7f, %10.7f ] ] # (%3.0f) %d %s\n" % (x.real, x.imag, (np.angle(x) / np.pi * 180) % 360, j + 1, symbol)) pass
class GroupVelocity(object): """ d omega ---- ------- = \ / omega d q \/q Gradient of omega in reciprocal space. d D(q) <e(q,nu)|------|e(q,nu)> d q """ def __init__(self, dynamical_matrix, q_length=None, symmetry=None, frequency_factor_to_THz=VaspToTHz, cutoff_frequency=1e-4): """ q_points is a list of sets of q-point and q-direction: [[q-point, q-direction], [q-point, q-direction], ...] q_length is used such as D(q + q_length) - D(q - q_length). """ self._dynmat = dynamical_matrix primitive = dynamical_matrix.get_primitive() self._reciprocal_lattice_inv = primitive.get_cell() self._reciprocal_lattice = np.linalg.inv(self._reciprocal_lattice_inv) self._q_length = q_length if self._dynmat.is_nac() and self._dynmat.get_nac_method() == 'gonze': if self._q_length is None: self._q_length = 1e-5 if self._q_length is None: self._ddm = DerivativeOfDynamicalMatrix(dynamical_matrix) else: self._ddm = None self._symmetry = symmetry self._factor = frequency_factor_to_THz self._cutoff_frequency = cutoff_frequency self._directions = np.array([[1, 2, 3], [1, 0, 0], [0, 1, 0], [0, 0, 1]], dtype='double') self._directions[0] /= np.linalg.norm(self._directions[0]) self._q_points = None self._group_velocity = None self._perturbation = None def set_q_points(self, q_points, perturbation=None): self._q_points = q_points self._perturbation = perturbation if perturbation is None: self._directions[0] = np.array([1, 2, 3]) else: self._directions[0] = np.dot( self._reciprocal_lattice, perturbation) self._directions[0] /= np.linalg.norm(self._directions[0]) self._set_group_velocity() def set_q_length(self, q_length): self._q_length = q_length def get_q_length(self): return self._q_length def get_group_velocity(self): return self._group_velocity def _set_group_velocity(self): gv = [self._set_group_velocity_at_q(q) for q in self._q_points] self._group_velocity = np.array(gv) def _set_group_velocity_at_q(self, q): self._dynmat.set_dynamical_matrix(q) dm = self._dynmat.get_dynamical_matrix() eigvals, eigvecs = np.linalg.eigh(dm) eigvals = eigvals.real freqs = np.sqrt(abs(eigvals)) * np.sign(eigvals) * self._factor gv = np.zeros((len(freqs), 3), dtype='double') deg_sets = degenerate_sets(freqs) ddms = self._get_dD(np.array(q)) pos = 0 for deg in deg_sets: gv[pos:pos+len(deg)] = self._perturb_D(ddms, eigvecs[:, deg]) pos += len(deg) for i, f in enumerate(freqs): if f > self._cutoff_frequency: gv[i, :] *= self._factor ** 2 / f / 2 else: gv[i, :] = 0 if self._perturbation is None: return self._symmetrize_group_velocity(gv, q) else: return gv def _symmetrize_group_velocity(self, gv, q): rotations = [] for r in self._symmetry.get_reciprocal_operations(): q_in_BZ = q - np.rint(q) diff = q_in_BZ - np.dot(r, q_in_BZ) if (np.abs(diff) < self._symmetry.get_symmetry_tolerance()).all(): rotations.append(r) gv_sym = np.zeros_like(gv) for r in rotations: r_cart = similarity_transformation(self._reciprocal_lattice, r) gv_sym += np.dot(r_cart, gv.T).T return gv_sym / len(rotations) def _get_dD(self, q): if self._q_length is None: return self._get_dD_analytical(q) else: return self._get_dD_FD(q) def _get_dD_FD(self, q): # finite difference ddm = [] for dqc in self._directions * self._q_length: dq = np.dot(self._reciprocal_lattice_inv, dqc) ddm.append(delta_dynamical_matrix(q, dq, self._dynmat) / self._q_length / 2) return np.array(ddm) def _get_dD_analytical(self, q): self._ddm.run(q) ddm = self._ddm.get_derivative_of_dynamical_matrix() dtype = "c%d" % (np.dtype('double').itemsize * 2) ddm_dirs = np.zeros((len(self._directions),) + ddm.shape[1:], dtype=dtype) for i, dq in enumerate(self._directions): for j in range(3): ddm_dirs[i] += dq[j] * ddm[j] return ddm_dirs def _perturb_D(self, ddms, eigsets): eigvals, eigvecs = np.linalg.eigh( np.dot(eigsets.T.conj(), np.dot(ddms[0], eigsets))) gv = [] rot_eigsets = np.dot(eigsets, eigvecs) for ddm in ddms[1:]: gv.append( np.diag(np.dot(rot_eigsets.T.conj(), np.dot(ddm, rot_eigsets))).real) return np.transpose(gv)