def _velocity(mode, hw, dDk, degenerate, project): r""" For modes in an orthogonal basis """ # Decouple the degenerate modes if not degenerate is None: for deg in degenerate: # Set the average frequency hw[deg] = np.average(hw[deg]) # Now diagonalize to find the contributions from individual modes # then re-construct the seperated degenerate modes # Since we do this for all directions we should decouple them all mode[deg] = _decouple_eigh(mode[deg], *dDk) cm = conj(mode) if project: v = np.empty([mode.shape[0], mode.shape[1], 3], dtype=dtype_complex_to_real(mode.dtype)) v[:, :, 0] = (cm * dDk[0].dot(mode.T).T).real v[:, :, 1] = (cm * dDk[1].dot(mode.T).T).real v[:, :, 2] = (cm * dDk[2].dot(mode.T).T).real else: v = np.empty([mode.shape[0], 3], dtype=dtype_complex_to_real(mode.dtype)) v[:, 0] = einsum('ij,ji->i', cm, dDk[0].dot(mode.T)).real v[:, 1] = einsum('ij,ji->i', cm, dDk[1].dot(mode.T)).real v[:, 2] = einsum('ij,ji->i', cm, dDk[2].dot(mode.T)).real # Set everything to zero for the negative frequencies v[hw < 0, ...] = 0 if project: return v * _velocity_const / (2 * hw.reshape(-1, 1, 1)) return v * _velocity_const / (2 * hw.reshape(-1, 1))
def _velocity(mode, hw, dDk, degenerate): r""" For modes in an orthogonal basis """ # Along all directions v = np.empty([mode.shape[0], 3], dtype=dtype_complex_to_real(mode.dtype)) # Decouple the degenerate modes if not degenerate is None: for deg in degenerate: # Set the average frequency hw[deg] = np.average(hw[deg]) # Now diagonalize to find the contributions from individual modes # then re-construct the seperated degenerate modes # Since we do this for all directions we should decouple them all vv = conj(mode[deg, :]).dot(dDk[0].dot(mode[deg, :].T)) S = eigh_destroy(vv)[1].T.dot(mode[deg, :]) vv = conj(S).dot((dDk[1]).dot(S.T)) S = eigh_destroy(vv)[1].T.dot(S) vv = conj(S).dot((dDk[2]).dot(S.T)) mode[deg, :] = eigh_destroy(vv)[1].T.dot(S) v[:, 0] = (conj(mode.T) * dDk[0].dot(mode.T)).sum(0).real v[:, 1] = (conj(mode.T) * dDk[1].dot(mode.T)).sum(0).real v[:, 2] = (conj(mode.T) * dDk[2].dot(mode.T)).sum(0).real # Set everything to zero for the negative frequencies v[hw < 0, :] = 0 return v * _velocity_const / (2 * hw.reshape(-1, 1))
def norm2(self, sum=True): r""" Return a vector with the norm of each state :math:`\langle\psi|\psi\rangle` Parameters ---------- sum : bool, optional if true the summed site square is returned (a vector). For false a matrix with normalization squared per site is returned. Returns ------- numpy.ndarray the normalization for each state """ if not sum: return (conj(self.state) * self.state.T).real dtype = dtype_complex_to_real(self.dtype) N = len(self) n = np.empty(N, dtype=dtype) for i in range(N): n[i] = _idot(self.state[i, :]).real return n
def test_dtype_complex_to_real(): for d in [np.int32, np.int64, np.float32, np.float64]: assert dtype_complex_to_real(d) == d assert dtype_complex_to_real(np.complex64) == np.float32 assert dtype_complex_to_real(np.complex128) == np.float64
def PDOS(E, eig, eig_v, S=None, distribution='gaussian', spin=None): r""" Calculate the projected density of states (PDOS) for a set of energies, `E`, with a distribution function The :math:`\mathrm{PDOS}(E)` is calculated as: .. math:: \mathrm{PDOS}_\nu(E) = \sum_i \psi^*_{i,\nu} [\mathbf S | \psi_{i}\rangle]_\nu D(E-\epsilon_i) where :math:`D(\Delta E)` is the distribution function used. Note that the distribution function used may be a user-defined function. Alternatively a distribution function may be aquired from `sisl.physics.distribution`. In case of an orthogonal basis set :math:`\mathbf S` is equal to the identity matrix. Note that `DOS` is the sum of the orbital projected DOS: .. math:: \mathrm{DOS}(E) = \sum_\nu\mathrm{PDOS}_\nu(E) For non-collinear calculations (this includes spin-orbit calculations) the PDOS is additionally separated into 4 components (in this order): - Total projected DOS - Projected spin magnetic moment along :math:`x` direction - Projected spin magnetic moment along :math:`y` direction - Projected spin magnetic moment along :math:`z` direction These are calculated using the Pauli matrices :math:`\boldsymbol\sigma_x`, :math:`\boldsymbol\sigma_y` and :math:`\boldsymbol\sigma_z`: .. math:: \mathrm{PDOS}_\nu^\Sigma(E) &= \sum_i \psi^*_{i,\nu} \boldsymbol\sigma_z \boldsymbol\sigma_z [\mathbf S | \psi_{i}\rangle]_\nu D(E-\epsilon_i) \\ \mathrm{PDOS}_\nu^x(E) &= \sum_i \psi^*_{i,\nu} \boldsymbol\sigma_x [\mathbf S | \psi_{i}\rangle]_\nu D(E-\epsilon_i) \\ \mathrm{PDOS}_\nu^y(E) &= \sum_i \psi^*_{i,\nu} \boldsymbol\sigma_y [\mathbf S | \psi_{i}\rangle]_\nu D(E-\epsilon_i) \\ \mathrm{PDOS}_\nu^z(E) &= \sum_i \psi^*_{i,\nu} \boldsymbol\sigma_z [\mathbf S | \psi_{i}\rangle]_\nu D(E-\epsilon_i) Note that the total PDOS may be calculated using :math:`\boldsymbol\sigma_i\boldsymbol\sigma_i` where :math:`i` may be either of :math:`x`, :math:`y` or :math:`z`. Parameters ---------- E : array_like energies to calculate the projected-DOS from eig : array_like eigenvalues eig_v : array_like eigenvectors S : array_like, optional overlap matrix used in the :math:`\langle\psi|\mathbf S|\psi\rangle` calculation. If `None` the identity matrix is assumed. For non-collinear calculations this matrix may be halve the size of ``len(eig_v[0, :])`` to trigger the non-collinear calculation of PDOS. distribution : func or str, optional a function that accepts :math:`E-\epsilon` as argument and calculates the distribution function. spin : str or Spin, optional the spin configuration. This is generally only needed when the eigenvectors correspond to a non-collinear calculation. See Also -------- sisl.physics.distribution : a selected set of implemented distribution functions DOS : total DOS (same as summing over orbitals) spin_moment: spin moment for states Returns ------- numpy.ndarray projected DOS calculated at energies, has dimension ``(eig_v.shape[1], len(E))``. For non-collinear calculations it will be ``(4, eig_v.shape[1] // 2, len(E))``, ordered as indicated in the above list. """ if isinstance(distribution, str): distribution = get_distribution(distribution) # Figure out whether we are dealing with a non-collinear calculation if S is None: class S(object): __slots__ = [] shape = (eig_v.shape[1], eig_v.shape[1]) @staticmethod def dot(v): return v if spin is None: if S.shape[1] == eig_v.shape[1] // 2: spin = Spin('nc') S = S[::2, ::2] else: spin = Spin() # check for non-collinear (or SO) if spin.kind > Spin.POLARIZED: # Non colinear eigenvectors if S.shape[1] == eig_v.shape[1]: # Since we are going to reshape the eigen-vectors # to more easily get the mixed states, we can reduce the overlap matrix S = S[::2, ::2] # Initialize data PDOS = np.empty( [4, eig_v.shape[1] // 2, len(E)], dtype=dtype_complex_to_real(eig_v.dtype)) d = distribution(E - eig[0]).reshape(1, -1) v = S.dot(eig_v[0].reshape(-1, 2)) D = (conj(eig_v[0]) * v.ravel()).real.reshape(-1, 2) # diagonal PDOS PDOS[0, :, :] = D.sum(1).reshape(-1, 1) * d # total DOS PDOS[3, :, :] = (D[:, 0] - D[:, 1]).reshape(-1, 1) * d # z-dos D = (conj(eig_v[0, 1::2]) * 2 * v[:, 0]).reshape( -1, 1) # psi_down * psi_up * 2 PDOS[1, :, :] = D.real * d # x-dos PDOS[2, :, :] = D.imag * d # y-dos for i in range(1, len(eig)): d = distribution(E - eig[i]).reshape(1, -1) v = S.dot(eig_v[i].reshape(-1, 2)) D = (conj(eig_v[i]) * v.ravel()).real.reshape(-1, 2) PDOS[0, :, :] += D.sum(1).reshape(-1, 1) * d PDOS[3, :, :] += (D[:, 0] - D[:, 1]).reshape(-1, 1) * d D = (conj(eig_v[i, 1::2]) * 2 * v[:, 0]).reshape(-1, 1) PDOS[1, :, :] += D.real * d PDOS[2, :, :] += D.imag * d else: PDOS = (conj(eig_v[0]) * S.dot(eig_v[0])).real.reshape(-1, 1) \ * distribution(E - eig[0]).reshape(1, -1) for i in range(1, len(eig)): PDOS[:, :] += (conj(eig_v[i]) * S.dot(eig_v[i])).real.reshape(-1, 1) \ * distribution(E - eig[i]).reshape(1, -1) return PDOS
def spin_moment(eig_v, S=None): r""" Calculate the spin magnetic moment (also known as spin texture) This calculation only makes sense for non-collinear calculations. The returned quantities are given in this order: - Spin magnetic moment along :math:`x` direction - Spin magnetic moment along :math:`y` direction - Spin magnetic moment along :math:`z` direction These are calculated using the Pauli matrices :math:`\boldsymbol\sigma_x`, :math:`\boldsymbol\sigma_y` and :math:`\boldsymbol\sigma_z`: .. math:: \mathbf{S}_i^x &= \langle \psi_i | \boldsymbol\sigma_x \mathbf S | \psi_i \rangle \\ \mathbf{S}_i^y &= \langle \psi_i | \boldsymbol\sigma_y \mathbf S | \psi_i \rangle \\ \mathbf{S}_i^z &= \langle \psi_i | \boldsymbol\sigma_z \mathbf S | \psi_i \rangle Parameters ---------- eig_v : array_like vectors describing the electronic states S : array_like, optional overlap matrix used in the :math:`\langle\psi|\mathbf S|\psi\rangle` calculation. If `None` the identity matrix is assumed. The overlap matrix should correspond to the system and :math:`k` point the eigenvectors have been evaluated at. Notes ----- This routine cannot check whether the input eigenvectors originate from a non-collinear calculation. If a non-polarized eigenvector is passed to this routine, the output will have no physical meaning. See Also -------- DOS : total DOS PDOS : projected DOS Returns ------- numpy.ndarray spin moments per eigenvector with final dimension ``(eig_v.shape[0], 3)``. """ if eig_v.ndim == 1: return spin_moment(eig_v.reshape(1, -1), S).ravel() if S is None: class S(object): __slots__ = [] shape = (eig_v.shape[1] // 2, eig_v.shape[1] // 2) @staticmethod def dot(v): return v if S.shape[1] == eig_v.shape[1]: S = S[::2, ::2] # Initialize s = np.empty([eig_v.shape[0], 3], dtype=dtype_complex_to_real(eig_v.dtype)) # TODO consider doing this all in a few lines # TODO Since there are no energy dependencies here we can actually do all # TODO dot products in one go and then use b-casting rules. Should be much faster # TODO but also way more memory demanding! for i in range(len(eig_v)): v = S.dot(eig_v[i].reshape(-1, 2)) D = (conj(eig_v[i]) * v.ravel()).real.reshape(-1, 2) s[i, 2] = (D[:, 0] - D[:, 1]).sum() D = 2 * (conj(eig_v[i, 1::2]) * v[:, 0]).sum() s[i, 0] = D.real s[i, 1] = D.imag return s