def shift(self, E): r""" Shift the electronic structure by a constant energy This is equal to performing this operation: .. math:: \mathbf H_\sigma = \mathbf H_\sigma + E \mathbf S where :math:`\mathbf H_\sigma` correspond to the spin diagonal components of the Hamiltonian. Parameters ---------- E : float or (2,) the energy (in eV) to shift the electronic structure, if two values are passed the two first spin-components get shifted individually. """ E = _a.asarrayd(E) if E.size == 1: E = np.tile(E, 2) if np.abs(E).sum() == 0.: # When the energy is zero, there is no shift return if self.orthogonal: for i in range(self.shape[0]): for j in range(min(self.spin.spins, 2)): self[i, i, j] = self[i, i, j] + E[j] else: # For non-collinear and SO only the diagonal (real) components # should be shifted. for i in range(min(self.spin.spins, 2)): self._csr._D[:, i] += self._csr._D[:, self.S_idx] * E[i]
def spin_squared(self, k=(0, 0, 0), n_up=None, n_down=None, **kwargs): r""" Calculate spin-squared expectation value, see `~sisl.physics.electron.spin_squared` for details Parameters ---------- k : array_like, optional k-point at which the spin-squared expectation value is n_up : int, optional number of states for spin up configuration, default to all. All states up to and including `n_up`. n_down : int, optional same as `n_up` but for the spin-down configuration **kwargs : optional additional parameters passed to the `eigenstate` routine """ if not self.spin.is_polarized: raise ValueError(self.__class__.__name__ + '.spin_squared requires as spin-polarized system') es_alpha = self.eigenstate(k, spin=0, **kwargs) if not n_up is None: es_alpha = es_alpha.sub(range(n_up)) es_beta = self.eigenstate(k, spin=1, **kwargs) if not n_down is None: es_beta = es_beta.sub(range(n_down)) # es_alpha.Sk should equal es_beta.Sk, so just pass one of them return spin_squared(es_alpha.state, es_beta.state, es_alpha.Sk())
def outer(self, right=None, align=True): r""" Return the outer product by :math:`\sum_i|\psi_i\rangle\langle\psi'_i|` Parameters ---------- right : State, optional the right object to calculate the outer product of, if not passed it will do the outer product with itself. This object will always be the left :math:`|\psi_i\rangle` align : bool, optional first align `right` with the angles for this state (see `align`) Notes ----- This does *not* take into account a possible overlap matrix when non-orthogonal basis sets are used. Returns ------- numpy.ndarray a matrix with the sum of outer state products """ if right is None: m = _outer1(self.state[0, :]) for i in range(1, len(self)): m += _outer1(self.state[i, :]) else: if not np.array_equal(self.shape, right.shape): raise ValueError(self.__class__.__name__ + '.outer requires the objects to have the same shape') if align: # Align the states right = self.align_phase(right, copy=False) m = _outer(self.state[0, :], right.state[0, :]) for i in range(1, len(self)): m += _outer(self.state[i, :], right.state[i, :]) return m
def fromsp(cls, geom, P, S=None): """ Read and return the object with possible overlap """ # Calculate maximum number of connections per row nc = 0 # Ensure list of csr format (to get dimensions) if isspmatrix(P): P = [P] # Number of dimensions dim = len(P) # Sort all indices for the passed sparse matrices for i in range(dim): P[i] = P[i].tocsr() P[i].sort_indices() # Figure out the maximum connections per # row to reduce number of re-allocations to 0 for i in range(P[0].shape[0]): nc = max(nc, P[0][i, :].getnnz()) # Create the sparse object v = cls(geom, dim, P[0].dtype, nc, orthogonal=S is None) for i in range(dim): for jo, io, vv in ispmatrixd(P[i]): v[jo, io, i] = vv if not S is None: for jo, io, vv in ispmatrixd(S): v.S[jo, io] = vv return v
def fromsp(cls, geometry, P, S=None): """ Read and return the object with possible overlap """ # Calculate maximum number of connections per row nc = 0 # Ensure list of csr format (to get dimensions) if isspmatrix(P): P = [P] # Number of dimensions dim = len(P) # Sort all indices for the passed sparse matrices for i in range(dim): P[i] = P[i].tocsr() P[i].sort_indices() P[i].sum_duplicates() # Figure out the maximum connections per # row to reduce number of re-allocations to 0 for i in range(P[0].shape[0]): nc = max(nc, P[0][i, :].getnnz()) # Create the sparse object p = cls(geometry, dim, P[0].dtype, nc, orthogonal=S is None) if p._size != P[0].shape[0]: raise ValueError(cls.__name__ + '.fromsp cannot create a new class, the geometry ' + \ 'and sparse matrices does not have coinciding dimensions size != sp.shape[0]') for i in range(dim): ptr = P[i].indptr col = P[i].indices D = P[i].data # loop and add elements for r in range(p.shape[0]): sl = slice(ptr[r], ptr[r+1], None) p[r, col[sl], i] = D[sl] if not S is None: S = S.tocsr() S.sort_indices() S.sum_duplicates() ptr = S.indptr col = S.indices D = S.data # loop and add elements for r in range(p.shape[0]): sl = slice(ptr[r], ptr[r+1], None) p.S[r, col[sl]] = D[sl] return p
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 rotate(self, phi=0., individual=False): r""" Rotate all states (in-place) to rotate the largest component to be along the angle `phi` The states will be rotated according to: .. math:: S' = S / S^\dagger_{\phi-\mathrm{max}} \exp (i \phi), where :math:`S^\dagger_{\phi-\mathrm{max}}` is the phase of the component with the largest amplitude and :math:`\phi` is the angle to align on. Parameters ---------- phi : float, optional angle to align the state at (in radians), 0 is the positive real axis individual : bool, optional whether the rotation is per state, or a single maximum component is chosen. """ # Convert angle to complex phase phi = np.exp(1j * phi) s = self.state.view() if individual: for i in range(len(self)): # Find the maximum amplitude index idx = _argmax(_abs(s[i, :])) s[i, :] *= phi * _conj(s[i, idx] / _abs(s[i, idx])) else: # Find the maximum amplitude index among all elements idx = np.unravel_index(_argmax(_abs(s)), s.shape) s *= phi * _conj(s[idx] / _abs(s[idx]))
def read_geometry(self, *args, **kwargs): """ Returns the `Geometry` object from this file """ sc = self.read_supercell() xyz = _a.arrayd(np.copy(self._value('xa'))) xyz.shape = (-1, 3) # Create list with correct number of orbitals lasto = _a.arrayi(np.copy(self._value('lasto'))) nos = np.append([lasto[0]], np.diff(lasto)) nos = _a.arrayi(nos) if 'atom' in kwargs: # The user "knows" which atoms are present atms = kwargs['atom'] # Check that all atoms have the correct number of orbitals. # Otherwise we will correct them for i in range(len(atms)): if atms[i].no != nos[i]: atms[i] = Atom(atms[i].Z, [-1] * nos[i], tag=atms[i].tag) else: # Default to Hydrogen atom with nos[ia] orbitals # This may be counterintuitive but there is no storage of the # actual species atms = [Atom('H', [-1] * o) for o in nos] # Create and return geometry object geom = Geometry(xyz, atms, sc=sc) return geom
def read_hamiltonian(self, hermitian=True, dtype=np.float64, **kwargs): """ Reads a Hamiltonian (including the geometry) Reads the Hamiltonian model """ # Read the geometry in this file geom = self.read_geometry() # Rewind to ensure we can read the entire matrix structure self.fh.seek(0) # With the geometry in place we can read in the entire matrix # Create a new sparse matrix from scipy.sparse import lil_matrix H = lil_matrix((geom.no, geom.no_s), dtype=dtype) S = lil_matrix((geom.no, geom.no_s), dtype=dtype) def i2o(geom, i): try: # pure orbital return int(i) except: # ia[o] # atom ia and the orbital o j = i.replace('[', ' ').replace(']', ' ').split() return geom.a2o(int(j[0])) + int(j[1]) # Start reading in the supercell while True: found, l = self.step_to('matrix', reread=False) if not found: break # Get supercell ls = l.split() try: isc = np.array([int(ls[i]) for i in range(2, 5)], np.int32) except: isc = np.array([0, 0, 0], np.int32) off1 = geom.sc_index(isc) * geom.no off2 = geom.sc_index(-isc) * geom.no l = self.readline() while not l.startswith('end'): ls = l.split() jo = i2o(geom, ls[0]) io = i2o(geom, ls[1]) h = float(ls[2]) try: s = float(ls[3]) except: s = 0. H[jo, io + off1] = h S[jo, io + off1] = s if hermitian: S[io, jo + off2] = s H[io, jo + off2] = h l = self.readline() return Hamiltonian.fromsp(geom, H, S)
def align_norm(self, other, ret_index=False): r""" Align `other.state` with the site-norms for this state, a copy of `other` is returned with re-ordered states To determine the new ordering of `other` we first calculate the residual norm of the site-norms. .. math:: \delta N_{\alpha\beta} = \sum_i \big(\langle \psi^\alpha_i | \psi^\alpha_i\rangle - \langle \psi^\beta_i | \psi^\beta_i\rangle\big)^2 where :math:`\alpha` and :math:`\beta` correspond to state indices in `self` and `other`, respectively. The new states (from `other`) returned is then ordered such that the index :math:`\alpha \equiv \beta'` where :math:`\delta N_{\alpha\beta}` is smallest. Parameters ---------- other : State the other state to align onto this state ret_index : bool, optional also return indices for the swapped indices Returns ------- other_swap : State A swapped instance of `other` index : array of int the indices that swaps `other` to be ``other_swap``, i.e. ``other_swap = other.sub(index)`` Notes ----- The input state and output state have the same states, but their ordering is not necessarily the same. See Also -------- align_phase : rotate states such that their phases align """ snorm = self.norm2(False) onorm = other.norm2(False) # Now find new orderings show_warn = False idx = _a.fulli(len(other), -1) idxr = _a.emptyi(len(other)) for i in range(len(other)): R = ((snorm - onorm[i, :].reshape(1, -1)) ** 2).sum(1) # Figure out which band it should correspond to # find closest largest one for j in np.argsort(R): if j not in idx[:i]: idx[i] = j idxr[j] = i break show_warn = True if show_warn: warn(self.__class__.__name__ + '.align_norm found multiple possible candidates with minimal residue, swapping not unique') if ret_index: return other.sub(idxr), idxr return other.sub(idxr)
def shift(self, E): """ Shift the electronic structure by a constant energy Parameters ---------- E : float the energy (in eV) to shift the electronic structure """ if not self.orthogonal: # For non-colinear and SO only the diagonal components # should be shifted. for i in range(min(self.spin.spins, 2)): self._csr._D[:, i] -= self._csr._D[:, self.S_idx] * E else: for i in range(self.shape[0]): for j in range(min(self.spin.spins, 2)): self[i, i, j] = self[i, i, j] - E
def iter_shape(shape): """ Generator for iterating a shape by returning consecutive slices Parameters ---------- shape : array_like the shape of the iterator Yields ------ tuple of int a tuple of the same length as the input shape. The iterator is using the C-indexing. Examples -------- >>> for slc in iter_shape([2, 1, 3]): >>> print(slc) [0, 0, 0] [0, 0, 1] [0, 0, 2] [1, 0, 0] [1, 0, 1] [1, 0, 2] """ shape1 = [i - 1 for i in shape] ns = len(shape) ns1 = ns - 1 # Create list for iterating # we require a list because tuple's are immutable slc = [0] * ns while slc[0] < shape[0]: for i in range(shape[ns1]): slc[ns1] = i yield slc # Increment the previous shape indices for i in range(ns1, 0, -1): if slc[i] >= shape1[i]: slc[i] = 0 if i > 0: slc[i - 1] += 1
def _Pk_non_colinear_accummulate(self, k=(0, 0, 0), dtype=None, gauge='R', format='csr'): """ Sparse matrix (``scipy.sparse.csr_matrix``) at `k` for a non-collinear system Parameters ---------- k: array_like, optional k-point (default is Gamma point) dtype : numpy.dtype, optional default to `numpy.complex128` gauge : {'R', 'r'} chosen gauge """ if dtype is None: dtype = np.complex128 if np.dtype(dtype).kind != 'c': raise ValueError( "Non-colinear quantity setup requires a complex matrix") if gauge != 'R': raise ValueError('Only the cell vector gauge has been implemented') k = np.asarray(k, np.float64) k.shape = (-1, ) no = self.no # sparse matrix dimension (2 * self.no) V = csr_matrix((len(self), len(self)), dtype=dtype) v = [self.tocsr(i) for i in range(len(self.spin))] # Calculate all phases phases = np.exp( -1j * dot(dot(dot(self.rcell, k), self.cell), self.sc.sc_off.T)) # Now accummulate the matrix for si, phase in enumerate(phases): sl = slice(si * no, (si + 1) * no, None) # diagonal elements V[::2, ::2] += v[0][:, sl] * phase V[1::2, 1::2] += v[1][:, sl] * phase # off-diagonal elements vv = v[2][:, sl] - 1j * v[3][:, sl] V[1::2, ::2] += vv * phase V[::2, 1::2] += vv.conj() * phase del v return V.asformat(format)
def _Pk_spin_orbit_accummulate(self, k=(0, 0, 0), dtype=None, gauge='R', format='csr'): """ Sparse matrix (``scipy.sparse.csr_matrix``) at `k` for a spin-orbit system Parameters ---------- k: ``array_like``, `[0,0,0]` k-point dtype : ``numpy.dtype`` default to `numpy.complex128` gauge : str, 'R' chosen gauge """ if dtype is None: dtype = np.complex128 if np.dtype(dtype).kind != 'c': raise ValueError( "Spin orbit quantity setup requires a complex matrix") if gauge != 'R': raise ValueError('Only the cell vector gauge has been implemented') k = np.asarray(k, np.float64) k.shape = (-1, ) no = self.no # sparse matrix dimension (2 * self.no) V = csr_matrix((len(self), len(self)), dtype=dtype) v = [self.tocsr(i) for i in range(len(self.spin))] # Calculate all phases phases = np.exp( -1j * dot(dot(dot(self.rcell, k), self.cell), self.sc.sc_off.T)) # Now accummulate the matrix for si, phase in enumerate(phases): sl = slice(si * no, (si + 1) * no, None) # diagonal elements V[::2, ::2] += (v[0][:, sl] + 1j * v[4][:, sl]) * phase V[1::2, 1::2] = (v[1][:, sl] + 1j * v[5][:, sl]) * phase # off-diagonal elements V[1::2, ::2] = (v[2][:, sl] - 1j * v[3][:, sl]) * phase V[::2, 1::2] = (v[6][:, sl] + 1j * v[7][:, sl]) * phase del v return V.asformat(format)
def shift(self, E): """ Shift the electronic structure by a constant energy Parameters ---------- E : float or (2,) the energy (in eV) to shift the electronic structure, if two values are passed the two first spin-components get shifted individually. """ E = _a.asarrayd(E) if E.size == 1: E = np.tile(_a.asarrayd(E), 2) if not self.orthogonal: # For non-collinear and SO only the diagonal (real) components # should be shifted. for i in range(min(self.spin.spins, 2)): self._csr._D[:, i] += self._csr._D[:, self.S_idx] * E[i] else: for i in range(self.shape[0]): for j in range(min(self.spin.spins, 2)): self[i, i, j] = self[i, i, j] + E[i]
def _mulliken(self): # Calculate the Mulliken elements # First we re-create the sparse matrix as required for csr_matrix ptr = self._csr.ptr ncol = self._csr.ncol # Indices of non-zero elements idx = array_arange(ptr[:-1], n=ncol) # Create the new pointer array new_ptr = _a.emptyi(len(ptr)) new_ptr[0] = 0 col = self._csr.col[idx] _a.cumsumi(ncol, out=new_ptr[1:]) # The shape of the matrices shape = self.shape[:2] # Create list of charges to be returned Q = list() if self.orthogonal: # We only need the diagonal elements S = csr_matrix(shape, dtype=self.dtype) S.setdiag(1.) for i in range(self.shape[2]): DM = csr_matrix((self._csr._D[idx, i], col, new_ptr), shape=shape) Q.append(DM.multiply(S)) Q[-1].eliminate_zeros() else: # We now what S is and do it element-wise. q = self._csr._D[idx, :-1] * self._csr._D[idx, self.S_idx].reshape( -1, 1) for i in range(q.shape[1]): Q.append(csr_matrix((q[:, i], col, new_ptr), shape=shape)) Q[-1].eliminate_zeros() return Q
def iter(self, asarray=False): """ An iterator looping over the coefficients in this system Parameters ---------- asarray : bool, optional if true the yielded values are the coefficient vectors, i.e. a numpy array. Otherwise an equivalent object is yielded. Yields ------ coeff : Coefficent the current coefficent as an object, only returned if `asarray` is false. coeff : numpy.ndarray the current the coefficient as an array, only returned if `asarray` is true. """ if asarray: for i in range(len(self)): yield self.c[i] else: for i in range(len(self)): yield self.sub(i)
def iter(self, asarray=False): """ An iterator looping over the states in this system Parameters ---------- asarray : bool, optional if true the yielded values are the state vectors, i.e. a numpy array. Otherwise an equivalent object is yielded. Yields ------ state : State a state *only* containing individual elements, if `asarray` is false state : numpy.ndarray a state *only* containing individual elements, if `asarray` is true """ if asarray: for i in range(len(self)): yield self.state[i] else: for i in range(len(self)): yield self.sub(i)
def read_hessian(self, **kwargs): """ Returns a GULP Hessian matrix model for the output of GULP Parameters ---------- cutoff: float (0.001 eV/Ang**2) the cutoff of the force-constant matrix for adding to the matrix dtype: np.dtype (np.float64) default data-type of the matrix """ from scipy.sparse import diags dtype = kwargs.get('dtype', np.float64) geom = self.read_geometry(**kwargs) hessian = kwargs.get('hessian', None) if hessian is None: dyn = self._read_dyn(geom.no, **kwargs) else: dyn = get_sile(hessian, 'r').read_hessian(**kwargs) if dyn.shape[0] != geom.no: raise ValueError( "Inconsistent Hessian file, number of atoms not correct") # Perform mass scaling to retrieve the dynamical matrix mass = [geom.atom[ia].mass for ia in range(geom.na)] # Construct orbital mass mass = np.array(mass, np.float64).repeat(3) # Scale to get dynamical matrix dyn.data[:] /= np.sqrt(mass[dyn.row] * mass[dyn.col]) # slower, less memory consuming... #for I, ijd in enumerate(zip(dyn.row, dyn.col, dyn.data)): # dyn.data[I] = ijd[2] / sqrt(mass[ijd[0]] * mass[ijd[1]]) # clean-up del mass return Hessian.fromsp(geom, dyn)
def write_geometry(self, geom, fmt='.8f', **kwargs): """ Writes the geometry to the output file Parameters ---------- geom: Geometry The geometry we wish to write """ # The format of the geometry file is # for now, pretty stringent # Get cell_fmt cell_fmt = fmt if 'cell_fmt' in kwargs: cell_fmt = kwargs['cell_fmt'] xyz_fmt = fmt self._write('begin cell\n') # Write the cell fmt_str = ' {{0:{0}}} {{1:{0}}} {{2:{0}}}\n'.format(cell_fmt) for i in range(3): self._write(fmt_str.format(*geom.cell[i, :])) self._write('end cell\n') # Write number of super cells in each direction self._write('\nsupercell {0:d} {1:d} {2:d}\n'.format(*geom.nsc)) # Write all atomic positions along with the specie type self._write('\nbegin atom\n') fmt1_str = ' {{0:d}} {{1:{0}}} {{2:{0}}} {{3:{0}}}\n'.format(xyz_fmt) fmt2_str = ' {{0:d}}[{{1:d}}] {{2:{0}}} {{3:{0}}} {{4:{0}}}\n'.format( xyz_fmt) for ia in geom: Z = geom.atoms[ia].Z no = geom.atoms[ia].no if no == 1: self._write(fmt1_str.format(Z, *geom.xyz[ia, :])) else: self._write(fmt2_str.format(Z, no, *geom.xyz[ia, :])) self._write('end atom\n')
def DOS(E, eig, distribution='gaussian'): r""" Calculate the density of states (DOS) for a set of energies, `E`, with a distribution function The :math:`\mathrm{DOS}(E)` is calculated as: .. math:: \mathrm{DOS}(E) = \sum_i D(E-\epsilon_i) \approx\delta(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 retrieved from `sisl.physics.distribution`. Parameters ---------- E : array_like energies to calculate the DOS at eig : array_like eigenvalues distribution : func or str, optional a function that accepts :math:`E-\epsilon` as argument and calculates the distribution function. See Also -------- sisl.physics.distribution : a selected set of implemented distribution functions PDOS : projected DOS (same as this, but projected onto each orbital) spin_moment: spin moment for states Returns ------- numpy.ndarray : DOS calculated at energies, has same length as `E` """ if isinstance(distribution, str): distribution = get_distribution(distribution) DOS = distribution(E - eig[0]) for i in range(1, len(eig)): DOS += distribution(E - eig[i]) return DOS
def outer(self, idx=None): r""" Return the outer product for the indices `idx` (or all if ``None``) by :math:`\sum_i|\psi_i\rangle c_i\langle\psi_i|` Parameters ---------- idx : int or array_like, optional only perform an outer product of the specified indices, otherwise all states are used Returns ------- numpy.ndarray : a matrix with the sum of outer state products """ if idx is None: m = _couter1(self.c[0], self.state[0].ravel()) for i in range(1, len(self)): m += _couter1(self.c[i], self.state[i].ravel()) return m idx = _a.asarrayi(idx).ravel() m = _couter1(self.c[idx[0]], self.state[idx[0]].ravel()) for i in idx[1:]: m += _couter1(self.c[i], self.state[i].ravel()) return m
def write_hamiltonian(self, H, **kwargs): """ Writes Hamiltonian model to file Parameters ---------- H : Hamiltonian the model to be saved in the NC file spin : int, optional the spin-index of the Hamiltonian object that is stored. Default is the first index. """ # Ensure finalization H.finalize() # Ensure that the geometry is written self.write_geometry(H.geom) self._crt_dim(self, 'spin', len(H.spin)) # Determine the type of dH we are storing... k = kwargs.get('k', None) E = kwargs.get('E', None) ilvl, ik, iE = self._get_lvl_k_E(**kwargs) lvl = self._add_lvl(ilvl) # Append the sparsity pattern # Create basis group if 'n_col' in lvl.variables: if len(lvl.dimensions['nnzs']) != H.nnz: raise ValueError("The sparsity pattern stored in dH *MUST* be equivalent for " "all dH entries [nnz].") if np.any(lvl.variables['n_col'][:] != H._csr.ncol[:]): raise ValueError("The sparsity pattern stored in dH *MUST* be equivalent for " "all dH entries [n_col].") if np.any(lvl.variables['list_col'][:] != H._csr.col[:]+1): raise ValueError("The sparsity pattern stored in dH *MUST* be equivalent for " "all dH entries [list_col].") if np.any(lvl.variables['isc_off'][:] != H.geometry.sc.sc_off): raise ValueError("The sparsity pattern stored in dH *MUST* be equivalent for " "all dH entries [sc_off].") else: self._crt_dim(lvl, 'nnzs', H._csr.col.shape[0]) v = self._crt_var(lvl, 'n_col', 'i4', ('no_u',)) v.info = "Number of non-zero elements per row" v[:] = H._csr.ncol[:] v = self._crt_var(lvl, 'list_col', 'i4', ('nnzs',), chunksizes=(len(H._csr.col),), **self._cmp_args) v.info = "Supercell column indices in the sparse format" v[:] = H._csr.col[:] + 1 # correct for fortran indices v = self._crt_var(lvl, 'isc_off', 'i4', ('n_s', 'xyz')) v.info = "Index of supercell coordinates" v[:] = H.geometry.sc.sc_off[:, :] warn_E = True if ilvl in [3, 4]: if iE < 0: # We need to add the new value iE = len(lvl.variables['E']) lvl.variables['E'][iE] = E * eV2Ry warn_E = False warn_k = True if ilvl in [2, 4]: if ik < 0: ik = len(lvl.variables['kpt']) lvl.variables['kpt'][ik, :] = k warn_k = False if ilvl == 4 and warn_k and warn_E and False: # As soon as we have put the second k-point and the first energy # point, this warning will proceed... # I.e. even though the variable has not been set, it will WARN # Hence we out-comment this for now... warn(SileWarning('Overwriting k-point {0} and energy point {1} correction.'.format(ik, iE))) elif ilvl == 3 and warn_E: warn(SileWarning('Overwriting energy point {0} correction.'.format(iE))) elif ilvl == 2 and warn_k: warn(SileWarning('Overwriting k-point {0} correction.'.format(ik))) if ilvl == 1: dim = ('spin', 'nnzs') sl = [slice(None)] * 2 csize = [1] * 2 elif ilvl == 2: dim = ('nkpt', 'spin', 'nnzs') sl = [slice(None)] * 3 sl[0] = ik csize = [1] * 3 elif ilvl == 3: dim = ('ne', 'spin', 'nnzs') sl = [slice(None)] * 3 sl[0] = iE csize = [1] * 3 elif ilvl == 4: dim = ('nkpt', 'ne', 'spin', 'nnzs') sl = [slice(None)] * 4 sl[0] = ik sl[1] = iE csize = [1] * 4 # Number of non-zero elements csize[-1] = H.nnz if H.dtype.kind == 'c': v1 = self._crt_var(lvl, 'RedH', 'f8', dim, chunksizes=csize, attr = {'info': "Real part of dH", 'unit': "Ry"}, **self._cmp_args) for i in range(len(H.spin)): sl[-2] = i v1[sl] = H._csr._D[:, i].real * eV2Ry v2 = self._crt_var(lvl, 'ImdH', 'f8', dim, chunksizes=csize, attr = {'info': "Imaginary part of dH", 'unit': "Ry"}, **self._cmp_args) for i in range(len(H.spin)): sl[-2] = i v2[sl] = H._csr._D[:, i].imag * eV2Ry else: v = self._crt_var(lvl, 'dH', 'f8', dim, chunksizes=csize, attr = {'info': "dH", 'unit': "Ry"}, **self._cmp_args) for i in range(len(H.spin)): sl[-2] = i v[sl] = H._csr._D[:, i] * eV2Ry
def write_hamiltonian(self, ham, hermitian=True, **kwargs): """ Writes the Hamiltonian model to the file Writes a Hamiltonian model to the intrinsic Hamiltonian file format. The file can be constructed by the implict force of Hermiticity, or without. Utilizing the Hermiticity we reduce the file-size by approximately 50%. Parameters ---------- ham : `Hamiltonian` model hermitian : boolean=True whether the stored data is halved using the Hermitian property """ ham.finalize() # We use the upper-triangular form of the Hamiltonian # and the overlap matrix for hermitian problems geom = ham.geometry # First write the geometry self.write_geometry(geom, **kwargs) # We default to the advanced layuot if we have more than one # orbital on any one atom advanced = kwargs.get( 'advanced', np.any(np.array([a.no for a in geom.atom.atom], np.int32) > 1)) fmt = kwargs.get('fmt', 'g') if advanced: fmt1_str = ' {{0:d}}[{{1:d}}] {{2:d}}[{{3:d}}] {{4:{0}}}\n'.format( fmt) fmt2_str = ' {{0:d}}[{{1:d}}] {{2:d}}[{{3:d}}] {{4:{0}}} {{5:{0}}}\n'.format( fmt) else: fmt1_str = ' {{0:d}} {{1:d}} {{2:{0}}}\n'.format(fmt) fmt2_str = ' {{0:d}} {{1:d}} {{2:{0}}} {{3:{0}}}\n'.format(fmt) # We currently force the model to be finalized # before we can write it # This should be easily circumvented H = ham.tocsr(0) if not ham.orthogonal: S = ham.tocsr(ham.S_idx) # If the model is Hermitian we can # do with writing out half the entries if hermitian: herm_acc = kwargs.get('herm_acc', 1e-6) # We check whether it is Hermitian (not S) for i, isc in enumerate(geom.sc.sc_off): oi = i * geom.no oj = geom.sc_index(-isc) * geom.no # get the difference between the ^\dagger elements diff = H[:, oi:oi + geom.no] - \ H[:, oj:oj + geom.no].transpose() diff.eliminate_zeros() if np.any(np.abs(diff.data) > herm_acc): amax = np.amax(np.abs(diff.data)) warn( SileWarning( 'The model could not be asserted to be Hermitian ' 'within the accuracy required ({0}).'.format( amax))) hermitian = False del diff if hermitian: # Remove all double stuff for i, isc in enumerate(geom.sc.sc_off): if np.any(isc < 0): # We have ^\dagger element, remove it o = i * geom.no # Ensure that we remove all nullified quantities # (setting elements to zero will add them internally # :(, hence this actually constructs the full matrix # Therefore we do it on a row basis, to limit memory # requirements for j in range(geom.no): H[j, o:o + geom.no] = 0. H.eliminate_zeros() if not ham.orthogonal: S[j, o:o + geom.no] = 0. S.eliminate_zeros() o = geom.sc_index(np.zeros([3], np.int32)) # Get upper-triangular matrix of the unit-cell H and S ut = triu(H[:, o:o + geom.no], k=0).tocsr() for j in range(geom.no): H[j, o:o + geom.no] = 0. H[j, o:o + geom.no] = ut[j, :] H.eliminate_zeros() if not ham.orthogonal: ut = triu(S[:, o:o + geom.no], k=0).tocsr() for j in range(geom.no): S[j, o:o + geom.no] = 0. S[j, o:o + geom.no] = ut[j, :] S.eliminate_zeros() # Ensure that S and H have the same sparsity pattern for jo, io in ispmatrix(S): H[jo, io] = H[jo, io] del ut # Start writing of the model # We loop on all super-cells for i, isc in enumerate(geom.sc.sc_off): # Check that we have any contributions in this # sub-section Hsub = H[:, i * geom.no:(i + 1) * geom.no] if not ham.orthogonal: Ssub = S[:, i * geom.no:(i + 1) * geom.no] if Hsub.getnnz() == 0: continue # We have a contribution, write out the information self._write('\nbegin matrix {0:d} {1:d} {2:d}\n'.format(*isc)) if advanced: for jo, io, h in ispmatrixd(Hsub): o = np.array([jo, io], np.int32) a = geom.o2a(o) o = o - geom.a2o(a) if not ham.orthogonal: s = Ssub[jo, io] elif jo == io: s = 1. else: s = 0. if s == 0.: self._write(fmt1_str.format(a[0], o[0], a[1], o[1], h)) else: self._write( fmt2_str.format(a[0], o[0], a[1], o[1], h, s)) else: for jo, io, h in ispmatrixd(Hsub): if not ham.orthogonal: s = Ssub[jo, io] elif jo == io: s = 1. else: s = 0. if s == 0.: self._write(fmt1_str.format(jo, io, h)) else: self._write(fmt2_str.format(jo, io, h, s)) self._write('end matrix {0:d} {1:d} {2:d}\n'.format(*isc))
def read_geometry(self): """ Reading a geometry in regular Hamiltonian format """ cell = np.zeros([3, 3], np.float64) Z = [] xyz = [] nsc = np.zeros([3], np.int32) def Z2no(i, no): try: # pure atomic number return int(i), no except Exception: # both atomic number and no j = i.replace('[', ' ').replace(']', ' ').split() return int(j[0]), int(j[1]) # The format of the geometry file is keys = ['atom', 'cell', 'supercell', 'nsc'] for _ in range(len(keys)): _, l = self.step_to(keys, case=False) l = l.strip() if 'supercell' in l.lower() or 'nsc' in l.lower(): # We have everything in one line l = l.split()[1:] for i in range(3): nsc[i] = int(l[i]) elif 'cell' in l.lower(): if 'begin' in l.lower(): for i in range(3): l = self.readline().split() cell[i, 0] = float(l[0]) cell[i, 1] = float(l[1]) cell[i, 2] = float(l[2]) self.readline() # step past the block else: # We have everything in one line l = l.split()[1:] for i in range(3): cell[i, i] = float(l[i]) # TODO incorporate rotations elif 'atom' in l.lower(): l = self.readline() while not l.startswith('end'): ls = l.split() try: no = int(ls[4]) except Exception: no = 1 z, no = Z2no(ls[0], no) Z.append({'Z': z, 'orbs': no}) xyz.append([float(f) for f in ls[1:4]]) l = self.readline() xyz = np.array(xyz, np.float64) xyz.shape = (-1, 3) self.readline() # step past the block # Return the geometry # Create list of atoms geom = Geometry(xyz, atom=Atom[Z], sc=SuperCell(cell, nsc)) return geom
def write_delta(self, delta, **kwargs): r""" Writes a :math:`\delta` Hamiltonian to the file This term may be of - level-1: no E or k dependence - level-2: k-dependent - level-3: E-dependent - level-4: k- and E-dependent Parameters ---------- delta : SparseOrbitalBZSpin the model to be saved in the NC file k : array_like, optional a specific k-point :math:`\delta` term. I.e. only save the :math:`\delta` term for the given k-point. May be combined with `E` for a specific k and energy point. E : float, optional an energy dependent :math:`\delta` term. I.e. only save the :math:`\delta` term for the given energy. May be combined with `k` for a specific k and energy point. """ # Ensure finalization delta.finalize() # Ensure that the geometry is written self.write_geometry(delta.geom) self._crt_dim(self, 'spin', len(delta.spin)) # Determine the type of delta we are storing... k = kwargs.get('k', None) E = kwargs.get('E', None) ilvl, ik, iE = self._get_lvl_k_E(**kwargs) lvl = self._add_lvl(ilvl) # Append the sparsity pattern # Create basis group if 'n_col' in lvl.variables: if len(lvl.dimensions['nnzs']) != delta.nnz: raise ValueError( "The sparsity pattern stored in delta *MUST* be equivalent for " "all delta entries [nnz].") if np.any(lvl.variables['n_col'][:] != delta._csr.ncol[:]): raise ValueError( "The sparsity pattern stored in delta *MUST* be equivalent for " "all delta entries [n_col].") if np.any(lvl.variables['list_col'][:] != delta._csr.col[:] + 1): raise ValueError( "The sparsity pattern stored in delta *MUST* be equivalent for " "all delta entries [list_col].") if np.any(lvl.variables['isc_off'][:] != delta.geometry.sc.sc_off): raise ValueError( "The sparsity pattern stored in delta *MUST* be equivalent for " "all delta entries [sc_off].") else: self._crt_dim(lvl, 'nnzs', delta.nnz) v = self._crt_var(lvl, 'n_col', 'i4', ('no_u', )) v.info = "Number of non-zero elements per row" v[:] = delta._csr.ncol[:] v = self._crt_var(lvl, 'list_col', 'i4', ('nnzs', ), chunksizes=(delta.nnz, ), **self._cmp_args) v.info = "Supercell column indices in the sparse format" v[:] = delta._csr.col[:] + 1 # correct for fortran indices v = self._crt_var(lvl, 'isc_off', 'i4', ('n_s', 'xyz')) v.info = "Index of supercell coordinates" v[:] = delta.geometry.sc.sc_off[:, :] warn_E = True if ilvl in [3, 4]: if iE < 0: # We need to add the new value iE = lvl.variables['E'].shape[0] lvl.variables['E'][iE] = E * eV2Ry warn_E = False warn_k = True if ilvl in [2, 4]: if ik < 0: ik = lvl.variables['kpt'].shape[0] lvl.variables['kpt'][ik, :] = k warn_k = False if ilvl == 4 and warn_k and warn_E and False: # As soon as we have put the second k-point and the first energy # point, this warning will proceed... # I.e. even though the variable has not been set, it will WARN # Hence we out-comment this for now... warn( SileWarning( 'Overwriting k-point {0} and energy point {1} correction.'. format(ik, iE))) elif ilvl == 3 and warn_E: warn( SileWarning( 'Overwriting energy point {0} correction.'.format(iE))) elif ilvl == 2 and warn_k: warn(SileWarning('Overwriting k-point {0} correction.'.format(ik))) if ilvl == 1: dim = ('spin', 'nnzs') sl = [slice(None)] * 2 csize = [1] * 2 elif ilvl == 2: dim = ('nkpt', 'spin', 'nnzs') sl = [slice(None)] * 3 sl[0] = ik csize = [1] * 3 elif ilvl == 3: dim = ('ne', 'spin', 'nnzs') sl = [slice(None)] * 3 sl[0] = iE csize = [1] * 3 elif ilvl == 4: dim = ('nkpt', 'ne', 'spin', 'nnzs') sl = [slice(None)] * 4 sl[0] = ik sl[1] = iE csize = [1] * 4 # Number of non-zero elements csize[-1] = delta.nnz if delta.dtype.kind == 'c': v1 = self._crt_var(lvl, 'Redelta', 'f8', dim, chunksizes=csize, attr={ 'info': "Real part of delta", 'unit': "Ry" }, **self._cmp_args) v2 = self._crt_var(lvl, 'Imdelta', 'f8', dim, chunksizes=csize, attr={ 'info': "Imaginary part of delta", 'unit': "Ry" }, **self._cmp_args) for i in range(len(delta.spin)): sl[-2] = i v1[sl] = delta._csr._D[:, i].real * eV2Ry v2[sl] = delta._csr._D[:, i].imag * eV2Ry else: v = self._crt_var(lvl, 'delta', 'f8', dim, chunksizes=csize, attr={ 'info': "delta", 'unit': "Ry" }, **self._cmp_args) for i in range(len(delta.spin)): sl[-2] = i v[sl] = delta._csr._D[:, i] * eV2Ry
def _read_class(self, cls, **kwargs): """ Reads a class model from a file """ # Ensure that the geometry is written geom = self.read_geometry() # Determine the type of delta we are storing... E = kwargs.get('E', None) ilvl, ik, iE = self._get_lvl_k_E(**kwargs) # Get the level lvl = self._get_lvl(ilvl) if iE < 0 and ilvl in [3, 4]: raise ValueError( "Energy {0} eV does not exist in the file.".format(E)) if ik < 0 and ilvl in [2, 4]: raise ValueError("k-point requested does not exist in the file.") if ilvl == 1: sl = [slice(None)] * 2 elif ilvl == 2: sl = [slice(None)] * 3 sl[0] = ik elif ilvl == 3: sl = [slice(None)] * 3 sl[0] = iE elif ilvl == 4: sl = [slice(None)] * 4 sl[0] = ik sl[1] = iE # Now figure out what data-type the delta is. if 'Redelta' in lvl.variables: # It *must* be a complex valued Hamiltonian is_complex = True dtype = np.complex128 elif 'delta' in lvl.variables: is_complex = False dtype = np.float64 # Get number of spins nspin = len(self.dimensions['spin']) # Now create the sparse matrix stuff (we re-create the # array, hence just allocate the smallest amount possible) C = cls(geom, nspin, nnzpr=1, dtype=dtype, orthogonal=True) C._csr.ncol = _a.arrayi(lvl.variables['n_col'][:]) # Update maximum number of connections (in case future stuff happens) C._csr.ptr = np.insert(_a.cumsumi(C._csr.ncol), 0, 0) C._csr.col = _a.arrayi(lvl.variables['list_col'][:]) - 1 # Copy information over C._csr._nnz = len(C._csr.col) C._csr._D = np.empty([C._csr.ptr[-1], nspin], dtype) if is_complex: for ispin in range(nspin): sl[-2] = ispin C._csr._D[:, ispin].real = lvl.variables['Redelta'][sl] * Ry2eV C._csr._D[:, ispin].imag = lvl.variables['Imdelta'][sl] * Ry2eV else: for ispin in range(nspin): sl[-2] = ispin C._csr._D[:, ispin] = lvl.variables['delta'][sl] * Ry2eV return C
def density(self, grid, spinor=None, tol=1e-7, eta=False): r""" Expand the density matrix to the charge density on a grid This routine calculates the real-space density components on a specified grid. This is an *in-place* operation that *adds* to the current values in the grid. Note: To calculate :math:`\rho(\mathbf r)` in a unit-cell different from the originating geometry, simply pass a grid with a unit-cell different than the originating supercell. The real-space density is calculated as: .. math:: \rho(\mathbf r) = \sum_{\nu\mu}\phi_\nu(\mathbf r)\phi_\mu(\mathbf r) D_{\nu\mu} While for non-collinear/spin-orbit calculations the density is determined from the spinor component (`spinor`) by .. math:: \rho_{\boldsymbol\sigma}(\mathbf r) = \sum_{\nu\mu}\phi_\nu(\mathbf r)\phi_\mu(\mathbf r) \sum_\alpha [\boldsymbol\sigma \mathbf \rho_{\nu\mu}]_{\alpha\alpha} Here :math:`\boldsymbol\sigma` corresponds to a spinor operator to extract relevant quantities. By passing the identity matrix the total charge is added. By using the Pauli matrix :math:`\boldsymbol\sigma_x` only the :math:`x` component of the density is added to the grid (see `Spin.X`). Parameters ---------- grid : Grid the grid on which to add the density (the density is in ``e/Ang^3``) spinor : (2,) or (2, 2), optional the spinor matrix to obtain the diagonal components of the density. For un-polarized density matrices this keyword has no influence. For spin-polarized it *has* to be either 1 integer or a vector of length 2 (defaults to total density). For non-collinear/spin-orbit density matrices it has to be a 2x2 matrix (defaults to total density). tol : float, optional DM tolerance for accepted values. For all density matrix elements with absolute values below the tolerance, they will be treated as strictly zeros. eta: bool, optional show a progressbar on stdout """ try: # Once unique has the axis keyword, we know we can safely # use it in this routine # Otherwise we raise an ImportError unique([[0, 1], [2, 3]], axis=0) except: raise NotImplementedError( self.__class__.__name__ + '.density requires numpy >= 1.13, either update ' 'numpy or do not use this function!') geometry = self.geometry # Check that the atomic coordinates, really are all within the intrinsic supercell. # If not, it may mean that the DM does not conform to the primary unit-cell paradigm # of matrix elements. It complicates things. fxyz = geometry.fxyz f_min = fxyz.min() f_max = fxyz.max() if f_min < 0 or 1. < f_max: warn( self.__class__.__name__ + '.density has been passed a geometry where some coordinates are ' 'outside the primary unit-cell. This may potentially lead to problems! ' 'Double check the charge density!') del fxyz, f_min, f_max # Extract sub variables used throughout the loop shape = _a.asarrayi(grid.shape) dcell = grid.dcell # Sparse matrix data csr = self._csr # In the following we don't care about division # So 1) save error state, 2) turn off divide by 0, 3) calculate, 4) turn on old error state old_err = np.seterr(divide='ignore', invalid='ignore') # Placeholder for the resulting coefficients DM = None if self.spin.kind > Spin.POLARIZED: if spinor is None: # Default to the total density spinor = np.identity(2, dtype=np.complex128) else: spinor = _a.arrayz(spinor) if spinor.size != 4 or spinor.ndim != 2: raise ValueError( self.__class__.__name__ + '.density with NC/SO spin, requires a 2x2 matrix.') DM = _a.emptyz([self.nnz, 2, 2]) idx = array_arange(csr.ptr[:-1], n=csr.ncol) if self.spin.kind == Spin.NONCOLINEAR: # non-collinear DM[:, 0, 0] = csr._D[idx, 0] DM[:, 1, 1] = csr._D[idx, 1] DM[:, 1, 0] = csr._D[idx, 2] - 1j * csr._D[idx, 3] #TODO check sign here! DM[:, 0, 1] = np.conj(DM[:, 1, 0]) else: # spin-orbit DM[:, 0, 0] = csr._D[idx, 0] + 1j * csr._D[idx, 4] DM[:, 1, 1] = csr._D[idx, 1] + 1j * csr._D[idx, 5] DM[:, 1, 0] = csr._D[idx, 2] - 1j * csr._D[idx, 3] #TODO check sign here! DM[:, 0, 1] = csr._D[idx, 6] + 1j * csr._D[idx, 7] # Perform dot-product with spinor, and take out the diagonal real part DM = dot(DM, spinor.T)[:, [0, 1], [0, 1]].sum(1).real elif self.spin.kind == Spin.POLARIZED: if spinor is None: spinor = _a.onesd(2) elif isinstance(spinor, Integral): # extract the provided spin-polarization s = _a.zerosd(2) s[spinor] = 1. spinor = s else: spinor = _a.arrayd(spinor) if spinor.size != 2 or spinor.ndim != 1: raise ValueError( self.__class__.__name__ + '.density with polarized spin, requires spinor ' 'argument as an integer, or a vector of length 2') idx = array_arange(csr.ptr[:-1], n=csr.ncol) DM = csr._D[idx, 0] * spinor[0] + csr._D[idx, 1] * spinor[1] else: idx = array_arange(csr.ptr[:-1], n=csr.ncol) DM = csr._D[idx, 0] # Create the DM csr matrix. csrDM = csr_matrix( (DM, csr.col[idx], np.insert(np.cumsum(csr.ncol), 0, 0)), shape=(self.shape[:2]), dtype=DM.dtype) # Clean-up del idx, DM # To heavily speed up the construction of the density we can recreate # the sparse csrDM matrix by summing the lower and upper triangular part. # This means we only traverse the sparse UPPER part of the DM matrix # I.e.: # psi_i * DM_{ij} * psi_j + psi_j * DM_{ji} * psi_i # is equal to: # psi_i * (DM_{ij} + DM_{ji}) * psi_j # Secondly, to ease the loops we extract the main diagonal (on-site terms) # and store this for separate usage csr_sum = [None] * geometry.n_s no = geometry.no primary_i_s = geometry.sc_index([0, 0, 0]) for i_s in range(geometry.n_s): # Extract the csr matrix o_start, o_end = i_s * no, (i_s + 1) * no csr = csrDM[:, o_start:o_end] if i_s == primary_i_s: csr_sum[i_s] = triu(csr) + tril(csr, -1).transpose() else: csr_sum[i_s] = csr # Recreate the column-stacked csr matrix csrDM = ss_hstack(csr_sum, format='csr') del csr, csr_sum # Remove all zero elements (note we use the tolerance here!) csrDM.data = np.where(np.fabs(csrDM.data) > tol, csrDM.data, 0.) # Eliminate zeros and sort indices etc. csrDM.eliminate_zeros() csrDM.sort_indices() csrDM.prune() # 1. Ensure the grid has a geometry associated with it sc = grid.sc.copy() if grid.geometry is None: # Create the actual geometry that encompass the grid ia, xyz, _ = geometry.within_inf(sc) if len(ia) > 0: grid.set_geometry(Geometry(xyz, geometry.atom[ia], sc=sc)) # Instead of looping all atoms in the supercell we find the exact atoms # and their supercell indices. add_R = _a.zerosd(3) + geometry.maxR() # Calculate the required additional vectors required to increase the fictitious # supercell by add_R in each direction. # For extremely skewed lattices this will be way too much, hence we make # them square. o = sc.toCuboid(True) sc = SuperCell(o._v, origo=o.origo) + np.diag(2 * add_R) sc.origo -= add_R # Retrieve all atoms within the grid supercell # (and the neighbours that connect into the cell) IA, XYZ, ISC = geometry.within_inf(sc) # Retrieve progressbar eta = tqdm_eta(len(IA), self.__class__.__name__ + '.density', 'atom', eta) cell = geometry.cell atom = geometry.atom axyz = geometry.axyz a2o = geometry.a2o def xyz2spherical(xyz, offset): """ Calculate the spherical coordinates from indices """ rx = xyz[:, 0] - offset[0] ry = xyz[:, 1] - offset[1] rz = xyz[:, 2] - offset[2] # Calculate radius ** 2 xyz_to_spherical_cos_phi(rx, ry, rz) return rx, ry, rz def xyz2sphericalR(xyz, offset, R): """ Calculate the spherical coordinates from indices """ rx = xyz[:, 0] - offset[0] idx = indices_fabs_le(rx, R) ry = xyz[idx, 1] - offset[1] ix = indices_fabs_le(ry, R) ry = ry[ix] idx = idx[ix] rz = xyz[idx, 2] - offset[2] ix = indices_fabs_le(rz, R) ry = ry[ix] rz = rz[ix] idx = idx[ix] if len(idx) == 0: return [], [], [], [] rx = rx[idx] # Calculate radius ** 2 ix = indices_le(rx**2 + ry**2 + rz**2, R**2) idx = idx[ix] if len(idx) == 0: return [], [], [], [] rx = rx[ix] ry = ry[ix] rz = rz[ix] xyz_to_spherical_cos_phi(rx, ry, rz) return idx, rx, ry, rz # Looping atoms in the sparse pattern is better since we can pre-calculate # the radial parts and then add them. # First create a SparseOrbital matrix, then convert to SparseAtom spO = SparseOrbital(geometry, dtype=np.int16) spO._csr = SparseCSR(csrDM) spA = spO.toSparseAtom(dtype=np.int16) del spO na = geometry.na # Remove the diagonal part of the sparse atom matrix off = na * primary_i_s for ia in range(na): del spA[ia, off + ia] # Get pointers and delete the atomic sparse pattern # The below complexity is because we are not finalizing spA csr = spA._csr a_ptr = np.insert(_a.cumsumi(csr.ncol), 0, 0) a_col = csr.col[array_arange(csr.ptr, n=csr.ncol)] del spA, csr # Get offset in supercell in orbitals off = geometry.no * primary_i_s origo = grid.origo # TODO sum the non-origo atoms to the csrDM matrix # this would further decrease the loops required. # Loop over all atoms in the grid-cell for ia, ia_xyz, isc in zip(IA, XYZ - origo.reshape(1, 3), ISC): # Get current atom ia_atom = atom[ia] IO = a2o(ia) IO_range = range(ia_atom.no) cell_offset = (cell * isc.reshape(3, 1)).sum(0) - origo # Extract maximum R R = ia_atom.maxR() if R <= 0.: warn("Atom '{}' does not have a wave-function, skipping atom.". format(ia_atom)) eta.update() continue # Retrieve indices of the grid for the atomic shape idx = grid.index(ia_atom.toSphere(ia_xyz)) # Now we have the indices for the largest orbital on the atom # Subsequently we have to loop the orbitals and the # connecting orbitals # Then we find the indices that overlap with these indices # First reduce indices to inside the grid-cell idx[idx[:, 0] < 0, 0] = 0 idx[shape[0] <= idx[:, 0], 0] = shape[0] - 1 idx[idx[:, 1] < 0, 1] = 0 idx[shape[1] <= idx[:, 1], 1] = shape[1] - 1 idx[idx[:, 2] < 0, 2] = 0 idx[shape[2] <= idx[:, 2], 2] = shape[2] - 1 # Remove duplicates, requires numpy >= 1.13 idx = unique(idx, axis=0) if len(idx) == 0: eta.update() continue # Get real-space coordinates for the current atom # as well as the radial parts grid_xyz = dot(idx, dcell) # Perform loop on connection atoms # Allocate the DM_pj arrays # This will have a size equal to number of elements times number of # orbitals on this atom # In this way we do not have to calculate the psi_j multiple times DM_io = csrDM[IO:IO + ia_atom.no, :].tolil() DM_pj = _a.zerosd([ia_atom.no, grid_xyz.shape[0]]) # Now we perform the loop on the connections for this atom # Remark that we have removed the diagonal atom (it-self) # As that will be calculated in the end for ja in a_col[a_ptr[ia]:a_ptr[ia + 1]]: # Retrieve atom (which contains the orbitals) ja_atom = atom[ja % na] JO = a2o(ja) jR = ja_atom.maxR() # Get actual coordinate of the atom ja_xyz = axyz(ja) + cell_offset # Reduce the ia'th grid points to those that connects to the ja'th atom ja_idx, ja_r, ja_theta, ja_cos_phi = xyz2sphericalR( grid_xyz, ja_xyz, jR) if len(ja_idx) == 0: # Quick step continue # Loop on orbitals on this atom for jo in range(ja_atom.no): o = ja_atom.orbital[jo] oR = o.R # Downsize to the correct indices if jR - oR < 1e-6: ja_idx1 = ja_idx.view() ja_r1 = ja_r.view() ja_theta1 = ja_theta.view() ja_cos_phi1 = ja_cos_phi.view() else: ja_idx1 = indices_le(ja_r, oR) if len(ja_idx1) == 0: # Quick step continue # Reduce arrays ja_r1 = ja_r[ja_idx1] ja_theta1 = ja_theta[ja_idx1] ja_cos_phi1 = ja_cos_phi[ja_idx1] ja_idx1 = ja_idx[ja_idx1] # Calculate the psi_j component psi = o.psi_spher(ja_r1, ja_theta1, ja_cos_phi1, cos_phi=True) # Now add this orbital to all components for io in IO_range: DM_pj[io, ja_idx1] += DM_io[io, JO + jo] * psi # Temporary clean up del ja_idx, ja_r, ja_theta, ja_cos_phi del ja_idx1, ja_r1, ja_theta1, ja_cos_phi1, psi # Now we have all components for all orbitals connection to all orbitals on atom # ia. We simply need to add the diagonal components # Loop on the orbitals on this atom ia_r, ia_theta, ia_cos_phi = xyz2spherical(grid_xyz, ia_xyz) del grid_xyz for io in IO_range: # Only loop halve the range. # This is because: triu + tril(-1).transpose() # removes the lower half of the on-site matrix. for jo in range(io + 1, ia_atom.no): DM = DM_io[io, off + IO + jo] oj = ia_atom.orbital[jo] ojR = oj.R # Downsize to the correct indices if R - ojR < 1e-6: ja_idx1 = slice(None) ja_r1 = ia_r.view() ja_theta1 = ia_theta.view() ja_cos_phi1 = ia_cos_phi.view() else: ja_idx1 = indices_le(ia_r, ojR) if len(ja_idx1) == 0: # Quick step continue # Reduce arrays ja_r1 = ia_r[ja_idx1] ja_theta1 = ia_theta[ja_idx1] ja_cos_phi1 = ia_cos_phi[ja_idx1] # Calculate the psi_j component DM_pj[io, ja_idx1] += DM * oj.psi_spher( ja_r1, ja_theta1, ja_cos_phi1, cos_phi=True) # Calculate the psi_i component # Note that this one *also* zeroes points outside the shell # I.e. this step is important because it "nullifies" all but points where # orbital io is defined. psi = ia_atom.orbital[io].psi_spher(ia_r, ia_theta, ia_cos_phi, cos_phi=True) DM_pj[io, :] += DM_io[io, off + IO + io] * psi DM_pj[io, :] *= psi # Temporary clean up ja_idx1 = ja_r1 = ja_theta1 = ja_cos_phi1 = None del ia_r, ia_theta, ia_cos_phi, psi, DM_io # Now add the density grid.grid[idx[:, 0], idx[:, 1], idx[:, 2]] += DM_pj.sum(0) # Clean-up del DM_pj, idx eta.update() eta.close() # Reset the error code for division np.seterr(**old_err)
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
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