def phase(self, method='max', return_indices=False): r""" Calculate the Euler angle (phase) for the elements of the state, in the range :math:`]-\pi;\pi]` Parameters ---------- method : {'max', 'all'} for max, the phase for the element which has the largest absolute magnitude is returned, for all, all phases are calculated return_indices : bool, optional return indices for the elements used when ``method=='max'`` """ if method == 'max': idx = _argmax(_abs(self.state), 1) if return_indices: return _phase(self.state[_a.arangei(len(self)), idx]), idx return _phase(self.state[_a.arangei(len(self)), idx]) elif method == 'all': return _phase(self.state) raise ValueError(f"{self.__class__.__name__}.phase only accepts method in [max, all]")
def _csr_from_sc_off(geom, sc_off, csr): """ Internal routine to convert *any* SparseCSR matrix from sisl nsc to siesta nsc """ nsc = geom.sc.nsc.astype(np.int32) sc = geom.sc.__class__([1], nsc=nsc) sc.sc_off = sc_off from_sc_off = sc.sc_index(geom.sc_off) # this transfers the local siesta csr matrix ordering to the geometry ordering col_from = (from_sc_off.reshape(-1, 1) * geom.no + _a.arangei(geom.no).reshape(1, -1)).ravel() _csr_from(col_from, csr)
def _Pk_dense(self, k=(0, 0, 0), dtype=None, gauge='R', format='csr', _dim=0): """ Sparse matrix (``scipy.sparse.csr_matrix``) at `k` for a polarized 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 gauge != 'R': raise ValueError('Only the cell vector gauge has been implemented') k = np.asarray(k, np.float64) k.shape = (-1, ) if not np.allclose(k, 0.): if np.dtype(dtype).kind != 'c': raise ValueError( self.__class__.__name__ + " setup at k different from Gamma requires a complex matrix" ) # sparse matrix dimension (self.no) V = np.empty([len(self), len(self)], dtype=dtype) # Calculate all phases phases = np.exp( -1j * dot(dot(dot(self.rcell, k), self.cell), self.sc.sc_off.T)) # Now create offsets offsets = -_a.arangei(0, len(phases) * self.no, self.no) # Do not cast to dtype, that is done below, then we retain precision diag = diags(phases, offsets, shape=(self.shape[1], self.shape[0])).toarray() V[:, :] = dot(self.tocsr(_dim).toarray(), diag) if format == 'array': return V elif format == 'dense': return np.asmatrix(V) # It must be a sparse matrix we inquire return csr_matrix(V).asformat(format)
def func(*args, coords=None, name=method.__name__, **kwargs): # xarray specific data (default to function name) bz = self._obj # retrieve ALL data array = array_func(*args, **kwargs) # Create coords if coords is None: coords = [('k', _a.arangei(len(bz)))] for i, v in enumerate(array.shape[1:]): coords.append((f"v{i+1}", _a.arangei(v))) else: coords = list(coords) coords.insert(0, ('k', _a.arangei(len(bz)))) for i in range(1, len(coords)): if isinstance(coords[i], str): coords[i] = (coords[i], _a.arangei(array.shape[i])) attrs = {'bz': bz, 'parent': bz.parent} return xarray.DataArray(array, coords=coords, name=name, attrs=attrs)
def remove(self, idx, axis): """ Removes certain indices from a specified axis. Works exactly opposite to `sub`. Parameters ---------- idx : array_like the indices of the grid axis `axis` to be removed axis : int the axis segment from which we remove all indices `idx` """ ret_idx = np.delete(_a.arangei(self.shape[axis]), _a.asarrayi(idx)) return self.sub(ret_idx, axis)
def sub_part(self, idx, axis, above): """ Retains parts of the grid via above/below designations. Works exactly opposite to `remove_part` Parameters ---------- idx : int the index of the grid axis `axis` to be retained for ``above=True`` grid[idx:,...] for ``above=False`` grid[:idx,...] axis : int the axis segment from which we retain the indices `idx` above: bool if ``True`` will retain the grid: ``grid[idx:,...]`` else it will retain the grid: ``grid[:idx,...]`` """ if above: sub = _a.arangei(idx, self.shape[axis]) else: sub = _a.arangei(0, idx) return self.sub(sub, axis)
def _displacement(mode, hw, mass): """ Real space displacements """ idx = (hw == 0).nonzero()[0] U = mode.copy() U[idx, :] = 0. # Now create the remaining displacements idx = delete(_a.arangei(mode.shape[0]), idx) # Generate displacement factor factor = _displacement_const / fabs(hw[idx]).reshape(-1, 1) ** 0.5 U.shape = (mode.shape[0], -1, 3) U[idx, :, :] = (mode[idx, :] * factor).reshape(-1, mass.shape[0], 3) / mass.reshape(1, -1, 1) ** 0.5 return U
def swapaxes(self, a, b): """ Returns Grid with swapped axis If ``swapaxes(0,1)`` it returns the 0 in the 1 values. """ # Create index vector idx = _a.arangei(3) idx[b] = a idx[a] = b s = np.copy(self.shape) d = self.__sc_geometry_dict() d['sc'] = d['sc'].swapaxes(a, b) grid = self.__class__(s[idx], bc=self.bc[idx], dtype=self.dtype, **d) # We need to force the C-order or we loose the contiguity grid.grid = np.copy(np.swapaxes(self.grid, a, b), order='C') return grid
def change_gauge(self, gauge): r""" In-place change of the gauge of the state coefficients The two gauges are related through: .. math:: \tilde C_j = e^{i\mathbf k\mathbf r_j} C_j where :math:`C_j` and :math:`\tilde C_j` belongs to the ``r`` and ``R`` gauge, respectively. Parameters ---------- gauge : {'R', 'r'} specify the new gauge for the mode coefficients """ # These calls will fail if the gauge is not specified. # In that case it will not do anything if self.info.get('gauge', gauge) == gauge: # Quick return return # Update gauge value self.info['gauge'] = gauge # Check that we can do a gauge transformation k = _a.asarrayd(self.info.get('k', [0., 0., 0.])) if k.dot(k) <= 0.000000001: return g = self.parent.geometry phase = g.xyz[g.o2a(_a.arangei(g.no)), :] @ (k @ g.rcell) try: if self.parent.spin.has_noncolinear: # for NC/SOC we have a 2x2 spin-box per orbital phase = np.repeat(phase, 2) except: pass if gauge == 'r': # R -> r gauge tranformation. self.state *= exp(-1j * phase).reshape(1, -1) elif gauge == 'R': # r -> R gauge tranformation. self.state *= exp(1j * phase).reshape(1, -1)
def real_space_coupling(self, ret_indices=False): """ Return the real-space coupling parent where they fold into the parent real-space unit cell The resulting parent object only contains the inner-cell couplings for the elements that couple out of the real-space matrix. Parameters ---------- ret_indices : bool, optional if true, also return the atomic indices (corresponding to `real_space_parent`) that encompass the coupling matrix Returns ------- parent : parent object only retaining the elements of the atoms that couple out of the primary unit cell atom_index : indices for the atoms that couple out of the geometry (`ret_indices`) """ opt = self._options s_ax = opt['semi_axis'] k_ax = opt['k_axis'] PC = self.parent.tile(max(1, self._unfold[s_ax]), s_ax).tile(self._unfold[k_ax], k_ax) # Geometry short-hand g = PC.geometry # Remove all inner-cell couplings (0, 0, 0) to figure out the # elements that couple out of the real-space region n = PC.shape[0] idx = g.sc.sc_index([0, 0, 0]) cols = _a.arangei(n) + idx * n csr = PC._csr.copy([0]) # we just want the sparse pattern, so forget about the other elements csr.delete_columns(cols, keep_shape=True) # Now PC only contains couplings along the k and semi-inf directions # Extract the connecting orbitals and reduce them to unique atomic indices orbs = g.osc2uc(csr.col[array_arange(csr.ptr[:-1], n=csr.ncol)], True) atom_idx = g.o2a(orbs, True) # Only retain coupling atoms PC = PC.sub(atom_idx) # Remove all out-of-cell couplings such that we only have inner-cell couplings. nsc = PC.nsc.copy() nsc[s_ax] = 1 nsc[k_ax] = 1 PC.set_nsc(nsc) if ret_indices: return PC, atom_idx return PC
def pivot(self, in_device=False, sort=False): """ Pivoting orbitals for the full system Parameters ---------- in_device : bool, optional whether the pivoting elements are with respect to the device region sort : bool, optional whether the pivoting elements are sorted """ if in_device and sort: return _a.arangei(self.no_d) pvt = self._value('pivot') - 1 if in_device: subn = _a.onesi(self.no) subn[pvt] = 0 pvt -= _a.cumsumi(subn)[pvt] elif sort: pvt = np.sort(pvt) return pvt
def change_gauge(self, gauge): r""" In-place change of the gauge of the mode coefficients The two gauges are related through: .. math:: \tilde C_j = e^{i\mathbf k\mathbf r_j} C_j where :math:`C_j` belongs to the gauge ``R`` and :math:`\tilde C_j` is in the gauge ``r``. Parameters ---------- gauge : {'R', 'r'} specify the new gauge for the mode coefficients """ # These calls will fail if the gauge is not specified. # In that case it will not do anything if self.info.get('gauge', gauge) == gauge: # Quick return return # Update gauge value self.info['gauge'] = gauge # Check that we can do a gauge transformation k = _a.asarrayd(self.info.get('k')) if (k**2).sum()**0.5 <= 0.000001: return g = self.parent.geometry phase = dot(g.xyz[g.o2a(_a.arangei(g.no)), :], dot(k, g.rcell)) if gauge == 'r': self.state *= np.exp(1j * phase).reshape(1, -1) elif gauge == 'R': self.state *= np.exp(-1j * phase).reshape(1, -1)
def _spectral_column(self, elec): # To calculate the full A we simply calculate the # G column where the electrode resides nb = len(self.btd) nbm1 = nb - 1 # These are the indices in the device (after pivoting) # So they refer to the idx = self.elec[elec].pvt_dev # Find parts we need to calculate block1 = (idx.min() < self.btd_cum).nonzero()[0][0] block2 = (idx.max() < self.btd_cum).nonzero()[0][0] if block1 == block2: blocks = [block1] else: blocks = [block1, block2] # We can only have 2 consecutive blocks for # a Gamma, so same for BTD assert len(blocks) <= 2 n = len(self.pvt) G = np.empty([n, len(idx)], dtype=self._data.A[0].dtype) c = np.append(0, self.btd_cum) A = self._data.A B = self._data.B C = self._data.C tX = self._data.tX tY = self._data.tY for b in blocks: # Find the indices in the block i = idx[c[b] <= idx] i = i[i < c[b + 1]].astype(np.int32) c_idx = _a.arangei(c[b], c[b + 1]).reshape(-1, 1) b_idx = indices_only(c_idx.ravel(), i) if b == blocks[0]: r_idx = np.arange(len(b_idx)) else: r_idx = np.arange(len(idx) - len(b_idx), len(idx)) sl = slice(c[b], c[b + 1]) if b == 0: G[sl, r_idx] = inv_destroy(A[b] - C[b + 1] @ tX[b])[:, b_idx] elif b == nbm1: G[sl, r_idx] = inv_destroy(A[b] - B[b - 1] @ tY[b])[:, b_idx] else: G[sl, r_idx] = inv_destroy(A[b] - B[b - 1] @ tY[b] - C[b + 1] @ tX[b])[:, b_idx] if len(blocks) == 1: break # Now calculate the thing (above below) sl = slice(c[b], c[b + 1]) if b == blocks[0]: # Calculate below slp = slice(c[b + 1], c[b + 2]) G[slp, r_idx] = - tX[b] @ G[sl, r_idx] else: # Calculate above slp = slice(c[b - 1], c[b]) G[slp, r_idx] = - tY[b] @ G[sl, r_idx] # Now we can calculate the Gf column above b = blocks[0] slp = slice(c[b], c[b + 1]) for b in range(blocks[0] - 1, -1, -1): sl = slice(c[b], c[b + 1]) G[sl, :] = - tY[b + 1] @ G[slp, :] slp = sl # All blocks below b = blocks[-1] slp = slice(c[b], c[b + 1]) for b in range(blocks[-1] + 1, nb): sl = slice(c[b], c[b + 1]) G[sl, :] = - tX[b - 1] @ G[slp, :] slp = sl # Now calculate the full spectral function return G @ self._data.gamma[elec] @ dagger(G)
def pivot(self, elec=None, in_device=False, sort=False): """ Return the pivoting indices for a specific electrode (in the device region) or the device Parameters ---------- elec : str or int the corresponding electrode to return the pivoting indices from in_device : bool, optional If ``True`` the pivoting table will be translated to the device region orbitals. If `sort` is also true, this would correspond to the orbitals directly translated to the geometry ``self.geometry.sub(self.a_dev)``. sort : bool, optional Whether the returned indices are sorted. Mostly useful if you want to handle the device in a non-pivoted order. Examples -------- >>> se = tbtncSileTBtrans(...) >>> se.pivot() [3, 4, 6, 5, 2] >>> se.pivot(sort=True) [2, 3, 4, 5, 6] >>> se.pivot(0) [2, 3] >>> se.pivot(0, in_device=True) [4, 0] >>> se.pivot(0, in_device=True, sort=True) [0, 1] >>> se.pivot(0, sort=True) [2, 3] See Also -------- pivot_down : for the pivot table for electrodes down-folding regions """ if elec is None: if in_device and sort: return _a.arangei(self.no_d) pvt = self._value('pivot') - 1 if in_device: # Count number of elements that we need to subtract from each orbital subn = _a.onesi(self.no) subn[pvt] = 0 pvt -= _a.cumsumi(subn)[pvt] elif sort: pvt = npsort(pvt) return pvt # Get electrode pivoting elements se_pvt = self._value('pivot', tree=self._elec(elec)) - 1 if sort: # Sort pivoting indices # Since we know that pvt is also sorted, then # the resulting in_device would also return sorted # indices se_pvt = npsort(se_pvt) if in_device: pvt = self._value('pivot') - 1 if sort: pvt = npsort(pvt) # translate to the device indices se_pvt = indices(pvt, se_pvt, 0) return se_pvt
def green(self, E, dtype=None): r""" Calculate the real-space Green function The real space Green function is calculated via: .. math:: \mathbf G^\mathcal{R}(E) = \sum_{\mathbf k} \mathbf G_{\mathbf k}(E) Parameters ---------- E : float/complex energy to evaluate the real-space Green function at dtype : numpy.dtype, optional the resulting data type, default to ``np.complex128`` """ opt = self._options # Retrieve integration k-grid bz = opt['bz'] try: # If the BZ implements TRS (MonkhorstPack) then force it trs = bz._trs except: trs = opt['trs'] if dtype is None: dtype = complex128 # Now we are to calculate the real-space self-energy if E.imag == 0: E = E.real + 1j * opt['eta'] # Used axes s_ax = opt['semi_axis'] k_ax = opt['k_axis'] # Calculation options # calculate both left and right at the same time. SE = self._calc['SE'].self_energy_lr # Define Bloch unfolding routine and number of tiles along the semi-inf direction unfold = self._unfold.copy() tile = unfold[s_ax] unfold[s_ax] = 1 bloch = Bloch(unfold) if tile == 1: # When not tiling, it can be simplified quite a bit M0 = self._calc['SE'].spgeom0 M0Pk = M0.Pk if self.parent.orthogonal: # Orthogonal *always* identity S0E = identity(len(M0), dtype=dtype) * E def _calc_green(k, no, tile, idx0): SL, SR = SE(E, k, dtype=dtype) return inv(S0E - M0Pk(k, dtype=dtype, format='array') - SL - SR, True) else: M0Sk = M0.Sk def _calc_green(k, no, tile, idx0): SL, SR = SE(E, k, dtype=dtype) return inv(M0Sk(k, dtype=dtype, format='array') * E - M0Pk(k, dtype=dtype, format='array') - SL - SR, True) else: M1 = self._calc['SE'].spgeom1 M1Pk = M1.Pk if self.parent.orthogonal: def _calc_green(k, no, tile, idx0): # Calculate left/right self-energies Gf, A2 = SE(E, k, dtype=dtype, bulk=True) # A1 == Gf, because of memory usage B = - M1Pk(k, dtype=dtype, format='array') # C = conjugate(B.T) tY = solve(Gf, conjugate(B.T), True, True) Gf = inv(A2 - dot(B, tY), True) tX = solve(A2, B, True, True) # Since this is the pristine case, we know that # G11 and G22 are the same: # G = [A1 - C.tX]^-1 == [A2 - B.tY]^-1 G = empty([tile, no, tile, no], dtype=dtype) G[idx0, :, idx0, :] = Gf.reshape(1, no, no) for i in range(1, tile): G[idx0[i:], :, idx0[:-i], :] = - dot(tX, G[i-1, :, 0, :]).reshape(1, no, no) G[idx0[:-i], :, idx0[i:], :] = - dot(tY, G[0, :, i-1, :]).reshape(1, no, no) return G.reshape(tile * no, -1) else: M1Sk = M1.Sk def _calc_green(k, no, tile, idx0): Gf, A2 = SE(E, k, dtype=dtype, bulk=True) # A1 == Gf, because of memory usage tY = M1Sk(k, dtype=dtype, format='array') # S tX = M1Pk(k, dtype=dtype, format='array') # H B = tY * E - tX # C = _conj(tY.T) * E - _conj(tX.T) tY = solve(Gf, conjugate(tY.T) * E - conjugate(tX.T), True, True) Gf = inv(A2 - dot(B, tY), True) tX = solve(A2, B, True, True) G = empty([tile, no, tile, no], dtype=dtype) G[idx0, :, idx0, :] = Gf.reshape(1, no, no) for i in range(1, tile): G[idx0[i:], :, idx0[:-i], :] = - dot(tX, G[i-1, :, 0, :]).reshape(1, no, no) G[idx0[:-i], :, idx0[i:], :] = - dot(tY, G[0, :, i-1, :]).reshape(1, no, no) return G.reshape(tile * no, -1) # Create functions used to calculate the real-space Green function # For TRS we only-calculate +k and average by using G(k) = G(-k)^T # The extra arguments is because the internal decorator is actually pretty slow # to filter out unused arguments. # If using Bloch's theorem we need to wrap the Green function calculation # as the method call. if len(bloch) > 1: def _func_bloch(k, no, tile, idx0, weight=None, parent=None): return bloch(_calc_green, k, no=no, tile=tile, idx0=idx0) else: _func_bloch = _calc_green # Tiling indices idx0 = _a.arangei(tile) no = len(self.parent) # calculate the Green function G = bz.asaverage().call(_func_bloch, no=no, tile=tile, idx0=idx0) if trs: # Faster to do it once, than per G return (G + G.T) * 0.5 return G
def _green_diag_block(self, idx): nb = len(self.btd) nbm1 = nb - 1 # Find parts we need to calculate block1 = (idx.min() < self.btd_cum).nonzero()[0][0] block2 = (idx.max() < self.btd_cum).nonzero()[0][0] if block1 == block2: blocks = [block1] else: blocks = list(range(block1, block2+1)) assert len(blocks) <= 2 n = self.btd[blocks].sum() G = np.empty([n, len(idx)], dtype=self._data.A[0].dtype) btd = self.btd c = np.append(0, self.btd_cum) A = self._data.A B = self._data.B C = self._data.C tX = self._data.tX tY = self._data.tY for b in blocks: # Find the indices in the block i = idx[c[b] <= idx].copy() i = i[i < c[b + 1]].astype(np.int32) c_idx = _a.arangei(c[b], c[b + 1]).reshape(-1, 1) b_idx = indices_only(c_idx.ravel(), i) # Subtract the first block to put it only in the sub-part c_idx -= c[blocks[0]] if b == blocks[0]: sl = slice(0, btd[b]) r_idx = np.arange(len(b_idx)) else: sl = slice(btd[blocks[0]], btd[blocks[0]] + btd[b]) r_idx = np.arange(len(idx) - len(b_idx), len(idx)) if b == 0: G[sl, r_idx] = inv_destroy(A[b] - C[b + 1] @ tX[b])[:, b_idx] elif b == nbm1: G[sl, r_idx] = inv_destroy(A[b] - B[b - 1] @ tY[b])[:, b_idx] else: G[sl, r_idx] = inv_destroy(A[b] - B[b - 1] @ tY[b] - C[b + 1] @ tX[b])[:, b_idx] if len(blocks) == 1: break # Now calculate the thing (below/above) if b == blocks[0]: # Calculate below slp = slice(btd[b], btd[b] + btd[blocks[1]]) G[slp, r_idx] = - tX[b] @ G[sl, r_idx] else: # Calculate above slp = slice(0, btd[blocks[0]]) G[slp, r_idx] = - tY[b] @ G[sl, r_idx] return blocks, G
def write_geometry(self, geometry, dynamic=True, group_species=False): r""" Writes the geometry to the contained file Parameters ---------- geometry : Geometry geometry to be written to the file dynamic : None, bool or list, optional define which atoms are dynamic in the VASP run (default is True, which means all atoms are dynamic). If None, the resulting file will not contain any dynamic flags group_species: bool, optional before writing `geometry` first re-order species to have species in consecutive blocks (see `geometry_group`) Examples -------- >>> car = carSileVASP('POSCAR', 'w') >>> geom = geom.graphene() >>> geom.write(car) # regular car without Selective Dynamics >>> geom.write(car, dynamic=False) # fix all atoms >>> geom.write(car, dynamic=[False, (True, False, True)]) # fix 1st and y coordinate of 2nd See Also -------- geometry_group: method used to group atoms together according to their species """ # Check that we can write to the file sile_raise_write(self) if group_species: geometry, idx = self.geometry_group(geometry, ret_index=True) else: # small hack to allow dynamic idx = _a.arangei(len(geometry)) # LABEL self._write('sisl output\n') # Scale self._write(' 1.\n') # Write unit-cell fmt = (' ' + '{:18.9f}' * 3) + '\n' for i in range(3): self._write(fmt.format(*geometry.cell[i])) # Figure out how many species pt = PeriodicTable() s, d = [], [] ia = 0 while ia < geometry.na: atom = geometry.atoms[ia] specie = geometry.atoms.specie[ia] ia_end = (np.diff(geometry.atoms.specie[ia:]) != 0).nonzero()[0] if len(ia_end) == 0: # remaining atoms ia_end = geometry.na else: ia_end = ia + ia_end[0] + 1 s.append(pt.Z_label(atom.Z)) d.append(ia_end - ia) ia += d[-1] fmt = ' {:s}' * len(d) + '\n' self._write(fmt.format(*s)) fmt = ' {:d}' * len(d) + '\n' self._write(fmt.format(*d)) if dynamic is None: # We write in direct mode dynamic = [None] * len(geometry) def todyn(fix): return '\n' else: self._write('Selective dynamics\n') b2s = {True: 'T', False: 'F'} def todyn(fix): if isinstance(fix, bool): return ' {0} {0} {0}\n'.format(b2s[fix]) return ' {} {} {}\n'.format(b2s[fix[0]], b2s[fix[1]], b2s[fix[2]]) self._write('Cartesian\n') if isinstance(dynamic, bool): dynamic = [dynamic] * len(geometry) fmt = '{:18.9f}' * 3 for ia in geometry: self._write( fmt.format(*geometry.xyz[ia, :]) + todyn(dynamic[idx[ia]]))
def read_scf(self, key="scf", iscf=-1, imd=None, as_dataframe=False): r""" Parse SCF information and return a table of SCF information depending on what is requested Parameters ---------- key : {'scf', 'ts-scf'} parse SCF information from Siesta SCF or TranSiesta SCF iscf : int, optional which SCF cycle should be stored. If ``-1`` only the final SCF step is stored, for None *all* SCF cycles are returned. When `iscf` values queried are not found they will be truncated to the nearest SCF step. imd: int or None, optional whether only a particular MD step is queried, if None, all MD steps are parsed and returned. A negative number wraps for the last MD steps. as_dataframe: boolean, optional whether the information should be returned as a `pandas.DataFrame`. The advantage of this format is that everything is indexed and therefore you know what each value means.You can also perform operations very easily on a dataframe. """ #These are the properties that are written in SIESTA scf props = ["iscf", "Eharris", "E_KS", "FreeEng", "dDmax", "Ef", "dHmax"] if not iscf is None: if iscf == 0: raise ValueError( f"{self.__class__.__name__}.read_scf requires iscf argument to *not* be 0!" ) if not imd is None: if imd == 0: raise ValueError( f"{self.__class__.__name__}.read_scf requires imd argument to *not* be 0!" ) def reset_d(d, line): if line.startswith('SCF cycle converged'): if len(d['data']) > 0: d['_final_iscf'] = 1 elif line.startswith('SCF cycle continued'): d['_final_iscf'] = 0 if key.lower() == 'scf': def parse_next(line, d): line = line.strip().replace('*', '0') reset_d(d, line) if line.startswith('ts-Vha:'): d['ts-Vha'] = float(line.split()[1]) elif line.startswith('scf:'): d['_found_iscf'] = True if len(line) == 97: # this should be for Efup/dwn # but I think this will fail for as_dataframe (TODO) data = [ int(line[5:9]), float(line[9:25]), float(line[25:41]), float(line[41:57]), float(line[57:67]), float(line[67:77]), float(line[77:87]), float(line[87:97]) ] elif len(line) == 87: data = [ int(line[5:9]), float(line[9:25]), float(line[25:41]), float(line[41:57]), float(line[57:67]), float(line[67:77]), float(line[77:87]) ] else: # Populate DATA by splitting data = line.split() data = [int(data[1])] + list(map(float, data[2:])) d['data'] = data elif key.lower() == 'ts-scf': props.append("ts-Vha") def parse_next(line, d): line = line.strip().replace('*', '0') reset_d(d, line) if line.startswith('ts-Vha:'): d['ts-Vha'] = float(line.split()[1]) elif line.startswith('ts-q:'): data = line.split()[1:] try: d['ts-q'] = list(map(float, data)) except: # We are probably reading a device list # ensure that props are appended if props[-1] != data[-1]: props.extend(data) pass elif line.startswith('ts-scf:'): d['_found_iscf'] = True if len(line) == 100: data = [ int(line[8:12]), float(line[12:28]), float(line[28:44]), float(line[44:60]), float(line[60:70]), float(line[70:80]), float(line[80:90]), float(line[90:100]), d['ts-Vha'] ] + d['ts-q'] elif len(line) == 90: data = [ int(line[8:12]), float(line[12:28]), float(line[28:44]), float(line[44:60]), float(line[60:70]), float(line[70:80]), float(line[80:90]), d['ts-Vha'] ] + d['ts-q'] else: # Populate DATA by splitting data = line.split() data = [int(data[1])] + list(map( float, data[2:])) + [d['ts-Vha']] + d['ts-q'] d['data'] = data # A temporary dictionary to hold information while reading the output file d = { '_found_iscf': False, '_final_iscf': 0, 'data': [], } md = [] scf = [] for line in self: parse_next(line, d) if d['_found_iscf']: d['_found_iscf'] = False data = d['data'] if len(data) == 0: continue if iscf is None or iscf < 0: scf.append(data) elif data[0] <= iscf: # this ensures we will retain the latest iscf in # case the requested iscf is too big scf = data if d['_final_iscf'] == 1: d['_final_iscf'] = 2 elif d['_final_iscf'] == 2: d['_final_iscf'] = 0 data = d['data'] if len(data) == 0: # this traps the case where we read ts-scf # but find the final scf iteration. # In that case we don't have any data. scf = [] continue if len(scf) == 0: # this traps cases where final_iscf has # been trickered but we haven't collected anything. # I.e. if key == scf but ts-scf also exists. continue # First figure out which iscf we should store if iscf is None: # or iscf > 0 # scf is correct pass elif iscf < 0: # truncate to 0 scf = scf[max(len(scf) + iscf, 0)] # Populate md md.append(np.array(scf)) # Reset SCF data scf = [] # In case we wanted a given MD step and it's this one, just stop reading # We are going to return the last MD (see below) if imd == len(md): break # Define the function that is going to convert the information of a MDstep to a Dataset if as_dataframe: import pandas as pd def MDstep_dataframe(scf): scf = np.atleast_2d(scf) return pd.DataFrame(scf[..., 1:], index=pd.Index(scf[..., 0].ravel().astype( np.int32), name="iscf"), columns=props[1:]) # Now we know how many MD steps there are # We will return stuff based on what the user requested # For pandas DataFrame this will be dependent # 1. all MD steps requested => imd == index, iscf == column (regardless of iscf==none|int) # 2. 1 MD step requested => iscf == index if imd is None: if as_dataframe: if len(md) == 0: # return an empty dataframe (with imd as index) return pd.DataFrame(index=pd.Index([], name="imd"), columns=props) # Regardless of what the user requests we will always have imd == index # and iscf a column, a user may easily change this. df = pd.concat(map(MDstep_dataframe, md), keys=_a.arangei(1, len(md) + 1), names=["imd"]) if iscf is not None: df.reset_index("iscf", inplace=True) return df if iscf is None: # since each MD step may be a different number of SCF steps # we cannot convert to a dense array return md return np.array(md) # correct imd to ensure we check against the final size imd = min(len(md) - 1, max(len(md) + imd, 0)) if len(md) == 0: # no data collected if as_dataframe: return pd.DataFrame(index=pd.Index([], name="iscf"), columns=props[1:]) return np.array(md[imd]) if imd > len(md): raise ValueError( f"{self.__class__.__name__}.read_scf could not find requested MD step ({imd})." ) # If a certain imd was requested, get it # Remember that if imd is positive, we stopped reading at the moment we reached it scf = np.array(md[imd]) if as_dataframe: return MDstep_dataframe(scf) return scf
def _csr_from(col_from, csr): """ Internal routine to convert columns in a SparseCSR matrix """ # local csr matrix ordering col_to = _a.arangei(csr.shape[1]) csr.translate_columns(col_from, col_to)
def read_data(self, as_dataarray=False): """ Returns data associated with the bands file Parameters -------- as_dataarray: boolean, optional if `True`, the information is returned as an `xarray.DataArray` Ticks (if read) are stored as an attribute of the DataArray (under `array.ticks` and `array.ticklabels`) """ band_lines = False # Luckily the data is in eV Ef = float(self.readline()) # Read the total length of the path (not used) _, _ = map(float, self.readline().split()) l = self.readline() try: _, _ = map(float, l.split()) band_lines = True except: # We are dealing with a band-points file pass # orbitals, n-spin, n-k if band_lines: l = self.readline() no, ns, nk = map(int, l.split()) # Create the data to contain all band points b = _a.emptyd([nk, ns, no]) # for band-lines if band_lines: k = _a.emptyd([nk]) for ik in range(nk): l = [float(x) for x in self.readline().split()] k[ik] = l[0] del l[0] # Now populate the eigenvalues while len(l) < ns * no: l.extend([float(x) for x in self.readline().split()]) l = _a.arrayd(l) l.shape = (ns, no) b[ik, :, :] = l[:, :] - Ef # Now we need to read the labels for the points xlabels = [] labels = [] nl = int(self.readline()) for _ in range(nl): l = self.readline().split() xlabels.append(float(l[0])) labels.append((' '.join(l[1:])).replace("'", '')) vals = (xlabels, labels), k, b else: k = _a.emptyd([nk, 3]) for ik in range(nk): l = [float(x) for x in self.readline().split()] k[ik, :] = l[0:3] del l[2] del l[1] del l[0] # Now populate the eigenvalues while len(l) < ns * no: l.extend([float(x) for x in self.readline().split()]) l = _a.arrayd(l) l.shape = (ns, no) b[ik, :, :] = l[:, :] - Ef vals = k, b if as_dataarray: from xarray import DataArray ticks = { "ticks": xlabels, "ticklabels": labels } if band_lines else {} return DataArray(b, name="Energy", attrs=ticks, coords=[("k", k), ("spin", _a.arangei(0, b.shape[1])), ("band", _a.arangei(0, b.shape[2]))]) return vals
def orbital_momentum(self, projection='orbital', method='onsite'): r""" Calculate orbital angular momentum on either atoms or orbitals Currently this implementation equals the Siesta implementation in that the on-site approximation is enforced thus limiting the calculated quantities to obey the following conditions: 1. Same atom 2. :math:`l>0` 3. :math:`l_\nu \equiv l_\mu` 4. :math:`m_\nu \neq m_\mu` 5. :math:`\zeta_\nu \equiv \zeta_\mu` This allows one to sum the orbital angular moments on a per atom site. Parameters ---------- projection : {'orbital', 'atom'} whether the angular momentum is resolved per atom, or per orbital method : {'onsite'} method used to calculate the angular momentum Returns ------- numpy.ndarray orbital angular momentum with the last dimension equalling the :math:`L_x`, :math:`L_y` and :math:`L_z` components """ # Check that the spin configuration is correct if not self.spin.is_spinorbit: raise ValueError( f"{self.__class__.__name__}.orbital_momentum requires a spin-orbit matrix" ) # First we calculate orb_lmZ = _a.emptyi([self.no, 3]) for atom, idx in self.geometry.atoms.iter(True): # convert to FIRST orbital index per atom oidx = self.geometry.a2o(idx) # loop orbitals for io, orb in enumerate(atom): orb_lmZ[oidx + io, :] = orb.l, orb.m, orb.Z # Now we need to calculate the stuff DM = self.copy() # The Siesta convention *only* calculates contributions # in the primary unit-cell. DM.set_nsc([1] * 3) geom = DM.geometry csr = DM._csr # The siesta moments are only *on-site* per atom. # 1. create a logical index for the matrix elements # that is true for ia-ia interaction and false # otherwise idx = repeat(_a.arangei(geom.no), csr.ncol) aidx = geom.o2a(idx) # Sparse matrix indices for data sidx = array_arange(csr.ptr[:-1], n=csr.ncol, dtype=np.int32) jdx = csr.col[sidx] ajdx = geom.o2a(jdx) # Now only take the elements that are *on-site* and which are *not* # having the same m quantum numbers (if the orbital index is the same # it means they have the same m quantum number) # # 1. on the same atom # 2. l > 0 # 3. same quantum number l # 4. different quantum number m # 5. same zeta onsite_idx = ((aidx == ajdx) & \ (orb_lmZ[idx, 0] > 0) & \ (orb_lmZ[idx, 0] == orb_lmZ[jdx, 0]) & \ (orb_lmZ[idx, 1] != orb_lmZ[jdx, 1]) & \ (orb_lmZ[idx, 2] == orb_lmZ[jdx, 2])).nonzero()[0] # clean variables we don't need del aidx, ajdx # Now reduce arrays to the orbital connections that obey the # above criteria idx = idx[onsite_idx] idx_l = orb_lmZ[idx, 0] idx_m = orb_lmZ[idx, 1] jdx = jdx[onsite_idx] jdx_m = orb_lmZ[jdx, 1] sidx = sidx[onsite_idx] # Sum the spin-box diagonal imaginary parts DM = csr._D[sidx][:, [4, 5]].sum(1) # Define functions to calculate L projections def La(idx_l, DM, sub): if len(sub) == 0: return [] return (idx_l[sub] * (idx_l[sub] + 1) * 0.5)**0.5 * DM[sub] def Lb(idx_l, DM, sub): if len(sub) == 0: return return (idx_l[sub] * (idx_l[sub] + 1) - 2)**0.5 * 0.5 * DM[sub] def Lc(idx, idx_l, DM, sub): if len(sub) == 0: return [], [] sub = sub[idx_l[sub] >= 3] if len(sub) == 0: return [], [] return idx[sub], (idx_l[sub] * (idx_l[sub] + 1) - 6)**0.5 * 0.5 * DM[sub] # construct for different m # in Siesta the spin orbital angular momentum # is calculated by swapping i and j indices. # This is somewhat confusing to me, so I reversed everything. # This will probably add to the confusion when comparing the two # Additionally Siesta calculates L for <i|L|j> and then does: # L(:) = [L(3), -L(2), -L(1)] # Here we *directly* store the quantities used. # Pre-allocate the L_xyz quantity per orbital. L = np.zeros([geom.no, 3]) L0 = L[:, 0] L1 = L[:, 1] L2 = L[:, 2] # Pre-calculate all those which have m_i + m_j == 0 b = (idx_m + jdx_m == 0).nonzero()[0] subtract.at(L2, idx[b], idx_m[b] * DM[b]) del b # mi == 0 i_m = idx_m == 0 # mj == -1 sub = logical_and(i_m, jdx_m == -1).nonzero()[0] subtract.at(L0, idx[sub], La(idx_l, DM, sub)) # mj == 1 sub = logical_and(i_m, jdx_m == 1).nonzero()[0] add.at(L1, idx[sub], La(idx_l, DM, sub)) # mi == 1 i_m = idx_m == 1 # mj == -2 sub = logical_and(i_m, jdx_m == -2).nonzero()[0] subtract.at(L0, idx[sub], Lb(idx_l, DM, sub)) # mj == 0 sub = logical_and(i_m, jdx_m == 0).nonzero()[0] subtract.at(L1, idx[sub], La(idx_l, DM, sub)) # mj == 2 sub = logical_and(i_m, jdx_m == 2).nonzero()[0] add.at(L1, idx[sub], Lb(idx_l, DM, sub)) # mi == -1 i_m = idx_m == -1 # mj == -2 sub = logical_and(i_m, jdx_m == -2).nonzero()[0] add.at(L1, idx[sub], Lb(idx_l, DM, sub)) # mj == 0 sub = logical_and(i_m, jdx_m == 0).nonzero()[0] add.at(L0, idx[sub], La(idx_l, DM, sub)) # mj == 2 sub = logical_and(i_m, jdx_m == 2).nonzero()[0] add.at(L0, idx[sub], Lb(idx_l, DM, sub)) # mi == 2 i_m = idx_m == 2 # mj == -3 sub = logical_and(i_m, jdx_m == -3).nonzero()[0] subtract.at(L0, *Lc(idx, idx_l, DM, sub)) # mj == -1 sub = logical_and(i_m, jdx_m == -1).nonzero()[0] subtract.at(L0, idx[sub], Lb(idx_l, DM, sub)) # mj == 1 sub = logical_and(i_m, jdx_m == 1).nonzero()[0] subtract.at(L1, idx[sub], Lb(idx_l, DM, sub)) # mj == 3 sub = logical_and(i_m, jdx_m == 3).nonzero()[0] add.at(L1, *Lc(idx, idx_l, DM, sub)) # mi == -2 i_m = idx_m == -2 # mj == -3 sub = logical_and(i_m, jdx_m == -3).nonzero()[0] add.at(L1, *Lc(idx, idx_l, DM, sub)) # mj == -1 sub = logical_and(i_m, jdx_m == -1).nonzero()[0] subtract.at(L1, idx[sub], Lb(idx_l, DM, sub)) # mj == 1 sub = logical_and(i_m, jdx_m == 1).nonzero()[0] add.at(L0, idx[sub], Lb(idx_l, DM, sub)) # mj == 3 sub = logical_and(i_m, jdx_m == 3).nonzero()[0] add.at(L0, *Lc(idx, idx_l, DM, sub)) # mi == -3 i_m = idx_m == -3 # mj == -2 sub = logical_and(i_m, jdx_m == -2).nonzero()[0] subtract.at(L1, *Lc(idx, idx_l, DM, sub)) # mj == 2 sub = logical_and(i_m, jdx_m == 2).nonzero()[0] add.at(L0, *Lc(idx, idx_l, DM, sub)) # mi == 3 i_m = idx_m == 3 # mj == -2 sub = logical_and(i_m, jdx_m == -2).nonzero()[0] subtract.at(L0, *Lc(idx, idx_l, DM, sub)) # mj == 2 sub = logical_and(i_m, jdx_m == 2).nonzero()[0] subtract.at(L1, *Lc(idx, idx_l, DM, sub)) if "orbital" == projection: return L elif "atom" == projection: # Now perform summation per atom l = np.zeros([geom.na, 3], dtype=L.dtype) add.at(l, geom.o2a(np.arange(geom.no)), L) return l raise ValueError( f"{self.__class__.__name__}.orbital_momentum must define projection to be 'orbital' or 'atom'." )