class TestSparseCSR(object): def setUp(self): self.s1 = SparseCSR((10, 100), dtype=np.int32) self.s2 = SparseCSR((10, 100, 2)) def test_init1(self): assert_equal(self.s1.dtype, np.int32) assert_equal(self.s2.dtype, np.float64) assert_true(np.allclose(self.s1.data, self.s1.data)) assert_true(np.allclose(self.s2.data, self.s2.data)) def test_init2(self): SparseCSR((10, 100)) for d in [np.int32, np.float64, np.complex128]: s = SparseCSR((10, 100), dtype=d) assert_equal(s.shape, (10, 100, 1)) assert_equal(s.dim, 1) assert_equal(s.dtype, d) for k in [1, 2]: s = SparseCSR((10, 100, k), dtype=d) assert_equal(s.shape, (10, 100, k)) assert_equal(s.dim, k) s = SparseCSR((10, 100), dim=k, dtype=d) assert_equal(s.shape, (10, 100, k)) assert_equal(s.dim, k) s = SparseCSR((10, 100, 3), dim=k, dtype=d) assert_equal(s.shape, (10, 100, 3)) assert_equal(s.dim, 3) def test_init3(self): csr = sc.sparse.csr_matrix((10, 10), dtype=np.int32) csr[0, 1] = 1 csr[0, 2] = 2 sp = SparseCSR(csr) assert_equal(sp.dtype, np.int32) assert_equal(sp.shape, (10, 10, 1)) assert_equal(len(sp), 2) assert_equal(sp[0, 1], 1) assert_equal(sp[0, 2], 2) sp = SparseCSR(csr, dtype=np.float64) assert_equal(sp.shape, (10, 10, 1)) assert_equal(sp.dtype, np.float64) assert_equal(len(sp), 2) assert_equal(sp[0, 1], 1) assert_equal(sp[0, 2], 2) def test_init4(self): csr = sc.sparse.csr_matrix((10, 10), dtype=np.int32) csr[0, 1] = 1 csr[0, 2] = 2 print(csr.indices, csr.indptr) sp = SparseCSR((csr.data, csr.indices, csr.indptr)) assert_equal(sp.dtype, np.int32) assert_equal(sp.shape, (10, 10, 1)) assert_equal(len(sp), 2) assert_equal(sp[0, 1], 1) assert_equal(sp[0, 2], 2) sp = SparseCSR((csr.data, csr.indices, csr.indptr), dtype=np.float64) assert_equal(sp.shape, (10, 10, 1)) assert_equal(sp.dtype, np.float64) assert_equal(len(sp), 2) assert_equal(sp[0, 1], 1) assert_equal(sp[0, 2], 2) def test_create1(self): self.s1[0, [1, 2, 3]] = 1 assert_equal(self.s1.nnz, 3) self.s1[2, [1, 2, 3]] = 1 assert_equal(self.s1.nnz, 6) self.s1.empty(keep=True) assert_equal(self.s1.nnz, 6) self.s1.empty() assert_equal(self.s1.nnz, 0) def test_create2(self): for i in range(10): j = range(i * 4, i * 4 + 3) self.s1[0, j] = i assert_equal(len(self.s1), (i + 1) * 3) for jj in j: assert_equal(self.s1[0, jj], i) assert_equal(self.s1[1, jj], 0) self.s1.empty() def test_create3(self): for i in range(10): j = range(i * 4, i * 4 + 3) self.s1[0, j] = i assert_equal(len(self.s1), (i + 1) * 3) self.s1[0, range((i + 1) * 4, (i + 1) * 4 + 3)] = None assert_equal(len(self.s1), (i + 1) * 3) for jj in j: assert_equal(self.s1[0, jj], i) assert_equal(self.s1[1, jj], 0) self.s1.empty() def test_finalize1(self): self.s1[0, [1, 2, 3]] = 1 self.s1[2, [1, 2, 3]] = 1. assert_false(self.s1.finalized) self.s1.finalize() assert_true(self.s1.finalized) self.s1.empty(keep=True) assert_true(self.s1.finalized) self.s1.empty() assert_false(self.s1.finalized) def test_delitem1(self): self.s1[0, [1, 2, 3]] = 1 assert_equal(len(self.s1), 3) del self.s1[0, 1] assert_equal(len(self.s1), 2) assert_equal(self.s1[0, 1], 0) assert_equal(self.s1[0, 2], 1) assert_equal(self.s1[0, 3], 1) self.s1[0, [1, 2, 3]] = 1 del self.s1[0, [1, 3]] assert_equal(len(self.s1), 1) assert_equal(self.s1[0, 1], 0) assert_equal(self.s1[0, 2], 1) assert_equal(self.s1[0, 3], 0) self.s1.empty() def test_op1(self): for i in range(10): j = range(i * 4, i * 4 + 3) self.s1[0, j] = i # i+ self.s1 += 1 for jj in j: assert_equal(self.s1[0, jj], i + 1) assert_equal(self.s1[1, jj], 0) # i- self.s1 -= 1 for jj in j: assert_equal(self.s1[0, jj], i) assert_equal(self.s1[1, jj], 0) # i* self.s1 *= 2 for jj in j: assert_equal(self.s1[0, jj], i * 2) assert_equal(self.s1[1, jj], 0) # // self.s1 //= 2 for jj in j: assert_equal(self.s1[0, jj], i) assert_equal(self.s1[1, jj], 0) # i** self.s1 **= 2 for jj in j: assert_equal(self.s1[0, jj], i**2) assert_equal(self.s1[1, jj], 0) def test_op2(self): for i in range(10): j = range(i * 4, i * 4 + 3) self.s1[0, j] = i # + s = self.s1 + 1 for jj in j: assert_equal(s[0, jj], i + 1) assert_equal(self.s1[0, jj], i) assert_equal(s[1, jj], 0) # - s = self.s1 - 1 for jj in j: assert_equal(s[0, jj], i - 1) assert_equal(self.s1[0, jj], i) assert_equal(s[1, jj], 0) # - (r) s = 1 - self.s1 for jj in j: assert_equal(s[0, jj], 1 - i) assert_equal(self.s1[0, jj], i) assert_equal(s[1, jj], 0) # * s = self.s1 * 2 for jj in j: assert_equal(s[0, jj], i * 2) assert_equal(self.s1[0, jj], i) assert_equal(s[1, jj], 0) # // s = s // 2 for jj in j: assert_equal(s[0, jj], i) assert_equal(self.s1[0, jj], i) assert_equal(s[1, jj], 0) # ** s = self.s1**2 for jj in j: assert_equal(s[0, jj], i**2) assert_equal(self.s1[0, jj], i) assert_equal(s[1, jj], 0) # ** (r) s = 2**self.s1 for jj in j: assert_equal(s[0, jj], 2**self.s1[0, jj]) assert_equal(self.s1[0, jj], i) assert_equal(s[1, jj], 0) def test_op3(self): S = SparseCSR((10, 100), dtype=np.int32) # Create initial stuff for i in range(10): j = range(i * 4, i * 4 + 3) S[0, j] = i for op in ['add', 'sub', 'mul', 'pow']: func = getattr(S, '__{}__'.format(op)) s = func(1) assert_equal(s.dtype, np.int32) s = func(1.) assert_equal(s.dtype, np.float64) if op != 'pow': s = func(1.j) assert_equal(s.dtype, np.complex128) S = S.copy(dtype=np.float64) for op in ['add', 'sub', 'mul', 'pow']: func = getattr(S, '__{}__'.format(op)) s = func(1) assert_equal(s.dtype, np.float64) s = func(1.) assert_equal(s.dtype, np.float64) if op != 'pow': s = func(1.j) assert_equal(s.dtype, np.complex128) S = S.copy(dtype=np.complex128) for op in ['add', 'sub', 'mul', 'pow']: func = getattr(S, '__{}__'.format(op)) s = func(1) assert_equal(s.dtype, np.complex128) s = func(1.) assert_equal(s.dtype, np.complex128) if op != 'pow': s = func(1.j) assert_equal(s.dtype, np.complex128) def test_op4(self): S = SparseCSR((10, 100), dtype=np.int32) # Create initial stuff for i in range(10): j = range(i * 4, i * 4 + 3) S[0, j] = i s = 1 + S assert_equal(s.dtype, np.int32) s = 1. + S assert_equal(s.dtype, np.float64) s = 1.j + S assert_equal(s.dtype, np.complex128) s = 1 - S assert_equal(s.dtype, np.int32) s = 1. - S assert_equal(s.dtype, np.float64) s = 1.j - S assert_equal(s.dtype, np.complex128) s = 1 * S assert_equal(s.dtype, np.int32) s = 1. * S assert_equal(s.dtype, np.float64) s = 1.j * S assert_equal(s.dtype, np.complex128) s = 1**S assert_equal(s.dtype, np.int32) s = 1.**S assert_equal(s.dtype, np.float64) s = 1.j**S assert_equal(s.dtype, np.complex128)
class Hamiltonian(object): """ Hamiltonian object containing the coupling constants between orbitals. The Hamiltonian object contains information regarding the - geometry - coupling constants between orbitals It contains an intrinsic sparse matrix of the Hamiltonian elements. Assigning or changing Hamiltonian elements is as easy as with standard ``numpy`` assignments: >>> ham = Hamiltonian(...) >>> ham.H[1,2] = 0.1 which assigns 0.1 as the coupling constant between orbital 2 and 3. (remember that Python is 0-based elements). """ # The order of the Energy # I.e. whether energy should be in other units than Ry # This conversion is made: [eV] ** _E_order _E_order = 1 def __init__(self, geom, nnzpr=None, orthogonal=True, spin=1, dtype=None, *args, **kwargs): """Create tight-binding model from geometry Initializes a tight-binding model using the :code:`geom` object as the underlying geometry for the tight-binding parameters. """ self._geom = geom # Initialize the sparsity pattern self.reset(nnzpr=nnzpr, orthogonal=orthogonal, spin=spin, dtype=dtype) def reset(self, nnzpr=None, orthogonal=True, spin=1, dtype=None): """ The sparsity pattern is cleaned and every thing is reset. The object will be the same as if it had been initialized with the same geometry as it were created with. Parameters ---------- nnzpr: int number of non-zero elements per row orthogonal: boolean, True if there is an overlap matrix associated with the Hamiltonian spin: int, 1 number of spin-components dtype: ``numpy.dtype``, `numpy.float64` the datatype of the Hamiltonian """ # I know that this is not the most efficient way to # access a C-array, however, for constructing a # sparse pattern, it should be faster if memory elements # are closer... # Hence, this choice of having H and S like this # We check the first atom and its neighbours, we then # select max(5,len(nc) * 4) if nnzpr is None: nnzpr = self.geom.close(0) if nnzpr is None: nnzpr = 8 else: nnzpr = max(5, len(nnzpr) * 4) self._orthogonal = orthogonal # Reset the sparsity pattern if not orthogonal: self._data = SparseCSR((self.no, self.no_s, spin + 1), nnzpr=nnzpr, dtype=dtype) else: self._data = SparseCSR((self.no, self.no_s, spin), nnzpr=nnzpr, dtype=dtype) self._spin = spin if spin == 1: self.UP = 0 self.DOWN = 0 self.S_idx = 1 self.Hk = self._Hk_unpolarized self.Sk = self._Sk elif spin == 2: self.UP = 0 self.DOWN = 1 self.S_idx = 2 self.Hk = self._Hk_polarized self.Sk = self._Sk elif spin == 4: self.Hk = self._Hk_non_collinear self.Sk = self._Sk_non_collinear self.S_idx = 4 elif spin == 8: self.Hk = self._Hk_spin_orbit self.Sk = self._Sk_non_collinear self.S_idx = 8 raise ValueError( "Currently the Hamiltonian has only been implemented with up to non-collinear spin." ) if orthogonal: # There is no overlap matrix self.S_idx = -1 def diagonal_Sk(self, k, dtype=None): """ For an orthogonal case we always return the identity matrix """ if dtype is None: dtype = np.float64 no = self.no S = csr_matrix((no, no), dtype=dtype) S.setdiag(1.) return S self.Sk = diagonal_Sk # Denote that one *must* specify all details of the elements self._def_dim = -1 def empty(self, keep=False): """ See `SparseCSR.empty` for details """ self._data.empty(keep) def copy(self, dtype=None): """ Return a copy of the ``Hamiltonian`` object """ if dtype is None: dtype = self.dtype H = self.__class__(self.geom, orthogonal=self.orthogonal, spin=self.spin, dtype=dtype) # Be sure to copy the content of the SparseCSR object H._data = self._data.copy(dtype=dtype) return H ######### Definitions of overrides ############ @property def geometry(self): """ Return the attached geometry """ return self._geom geom = geometry @property def spin(self): """ Return number of spin-components in Hamiltonian """ return self._spin @property def dtype(self): """ Return data type of Hamiltonian (and overlap matrix) """ return self._data.dtype @property def orthogonal(self): """ Return whether the Hamiltonian is orthogonal """ return self._orthogonal def __len__(self): """ Returns number of rows in the Hamiltonian """ return self.geom.no def __repr__(self): """ Representation of the tight-binding model """ s = self.geom.__repr__() return s + '\nNumber of spin / non-zero elements {0} / {1} '.format( self.spin, self.nnz) def __getattr__(self, attr): """ Returns the attributes from the underlying geometry Any attribute not found in the Hamiltonian class will be looked up in the underlying geometry. """ return getattr(self.geom, attr) def __getitem__(self, key): """ Return Hamiltonian coupling elements for the index(s) """ dd = self._def_dim if dd >= 0: key = tuple(key) + (dd, ) self._def_dim = -1 d = self._data[key] return d def __setitem__(self, key, val): """ Set or create couplings between orbitals in the Hamiltonian Override set item for slicing operations and enables easy setting of tight-binding parameters in a sparse matrix """ dd = self._def_dim if dd >= 0: key = tuple(key) + (dd, ) self._def_dim = -1 self._data[key] = val if dd < 0 and not self.orthogonal: warnings.warn(( 'Hamiltonian specification of both H and S simultaneously is deprecated. ' 'This functionality will be removed in a future release.')) def __get_H(self): self._def_dim = self.UP return self _get_H = __get_H def __set_H(self, key, value): if len(key) == 2: self._def_dim = self.UP self[key] = value _set_H = __set_H H = property(__get_H, __set_H) def __get_S(self): if self.orthogonal: return None self._def_dim = self.S_idx return self _get_S = __get_S def __set_S(self, key, value): if self.orthogonal: return None self._def_dim = self.S_idx self[key] = value _set_S = __set_S S = property(__get_S, __set_S) # Create iterations on entire set of orbitals def iter(self, local=False): """ Iterations of the orbital space in the geometry, two indices from loop An iterator returning the current atomic index and the corresponding orbital index. >>> for ia, io in self: In the above case `io` always belongs to atom `ia` and `ia` may be repeated according to the number of orbitals associated with the atom `ia`. Parameters ---------- local : `bool=False` whether the orbital index is the global index, or the local index relative to the atom it resides on. """ for ia, io in self.geom.iter_orbitals(local=local): yield ia, io __iter__ = iter # Create iterations on the non-zero elements def iter_nnz(self, atom=None, orbital=None): """ Iterations of the non-zero elements, returns a tuple of orbital and coupling orbital An iterator returning the current orbital index and the corresponding connected orbital where a non-zero is defined >>> for io, jo in self.iter_nnz(): In the above case `io` and `jo` are orbitals such that: >>> self.H[io,jo] returns the non-zero element of the Hamiltonian. One may reduce the iterated space by either requesting a specific set of atoms, or orbitals, _not_ both simultaneously. Examples -------- Looping only on one or more atoms: >>> for io, jo in self.iter_nnz(atom=[2, 3]): >>> # loop on all orbitals on atom 3 and 4 (0 indexing) >>> for io, jo in self.iter_nnz(orbital=[2, 3]): >>> # loop on orbitals 3 and 4 (0 indexing) Parameters ---------- atom : ``int``/``array_like`` iterate on couplings to the set of atoms (not compatible with `orbital`) orbital : ``int``/``array_like`` iterate on couplings to the set of orbitals (not compatible with `atom`) """ if atom is not None and orbital is not None: raise ValueError( "iter_nnz: both atom and orbital has been passed, only one allowed." ) if atom is not None: orbs = self.geom.a2o(atom, all=True) for io, jo in self._data.iter_nnz(orbs): yield io, jo elif orbital is not None: for io, jo in self._data.iter_nnz(orbital): yield io, jo else: for io, jo in self._data: yield io, jo def create_construct(self, dR, param): """ Returns a simple function for passing to the `construct` function. This is simply to leviate the creation of simplistic functions needed for setting up the Hamiltonian. Basically this returns a function: >>> def func(self, ia, idxs, idxs_xyz=None): >>> idx = self.geom.close(ia, dR=dR, idx=idxs) >>> for ix, p in zip(idx, param): >>> self[ia, ix] = p Note ---- This function only works for geometries with one orbital per atom. If you have more than one orbital on any atom, you should define your own function. Parameters ---------- dR : array_like radii parameters for tight-binding parameters. Must have same length as ``param`` or one less. If one less it will be extended with ``dR[0]/100`` param : array_like coupling constants corresponding to the ``dR`` ranges. ``param[0,:]`` are the tight-binding parameter for the all atoms within ``dR[0]`` of each atom. """ if self.orthogonal: def func(self, ia, idxs, idxs_xyz=None): idx = self.geom.close(ia, dR=dR, idx=idxs, idx_xyz=idxs_xyz) for ix, p in zip(idx, param): self[ia, ix] = p else: def func(self, ia, idxs, idxs_xyz=None): idx = self.geom.close(ia, dR=dR, idx=idxs, idx_xyz=idxs_xyz) for ix, p in zip(idx, param): self.H[ia, ix] = p[:-1] self.S[ia, ix] = p[-1] return func def construct(self, func, na_iR=1000, method='rand', eta=False): """ Automatically construct the Hamiltonian model based on a function that does the setting up of the Hamiltonian This may be called in two variants. 1. Pass a function (``func``), see e.g. ``create_construct`` which does the setting up. 2. Pass a tuple/list in ``func`` which consists of two elements, one is ``dR`` the radii parameters for the corresponding tight-binding parameters. The second is the tight-binding parameters corresponding to the ``dR[i]`` elements. In this second case all atoms must only have one orbital. Parameters ---------- func: callable or array_like this function *must* take 4 arguments. 1. Is the Hamiltonian object it-self (`self`) 2. Is the currently examined atom (`ia`) 3. Is the currently bounded indices (`idxs`) 4. Is the currently bounded indices atomic coordinates (`idxs_xyz`) An example `func` could be: >>> def func(self, ia, idxs, idxs_xyz=None): >>> idx = self.geom.close(ia, dR=[0.1, 1.44], idx=idxs, idx_xyz=idxs_xyz) >>> self.H[ia, idx[0]] = 0. # on-site >>> self.H[ia, idx[1]] = -2.7 # nearest-neighbour na_iR : int, 1000 number of atoms within the sphere for speeding up the `iter_block` loop. method : str, 'rand' method used in `Geometry.iter_block`, see there for details eta: bool, False whether an ETA will be printed """ if not callable(func): if not isinstance(func, (tuple, list)): raise ValueError( 'Passed `func` which is not a function, nor tuple/list of `dR, param`' ) if np.any(np.diff(self.geom.lasto) > 1): raise ValueError( "Automatically setting a tight-binding model " "for systems with atoms having more than 1 " "orbital *must* be done by your-self. You have to define a corresponding `func`." ) # Convert to a proper function func = self.create_construct(func[0], func[1]) iR = self.geom.iR(na_iR) # Get number of atoms na = len(self.geom) na_run = 0 from time import time from sys import stdout t0 = time() # Do the loop for ias, idxs in self.geom.iter_block(iR=iR, method=method): # Get all the indexed atoms... # This speeds up the searching for # coordinates... idxs_xyz = self.geom[idxs, :] # Loop the atoms inside for ia in ias: func(self, ia, idxs, idxs_xyz) if eta: # calculate the remaining atoms to process na_run += len(ias) na -= len(ias) t1 = time() # calculate hours, minutes, seconds m, s = divmod(float(t1 - t0) / na_run * na, 60) h, m = divmod(m, 60) stdout.write( "Hamiltonian.construct() ETA = {0:5d}h {1:2d}m {2:5.2f}s\r" .format(int(h), int(m), s)) stdout.flush() if eta: stdout.write("Hamiltonian.construct() {0:23s}\n".format('DONE')) stdout.flush() @property def finalized(self): """ Whether the contained data is finalized and non-used elements have been removed """ return self._data.finalized def finalize(self): """ Finalizes the tight-binding model Finalizes the tight-binding model so that no new sparse elements can be added. Sparse elements can still be changed. """ self._data.finalize() # Get the folded Hamiltonian at the Gamma point Hk = self.Hk() nzs = Hk.nnz if nzs != (Hk + Hk.T).nnz: warnings.warn( 'Hamiltonian does not retain symmetric couplings, this might be problematic.' ) @property def nnz(self): """ Returns number of non-zero elements in the tight-binding model """ return self._data.nnz @property def no(self): """ Returns number of orbitals as used when the object was created """ return self._data.nr def tocsr(self, index, isc=None): """ Return a ``scipy.sparse.csr_matrix`` from the specified index Parameters ---------- index : ``int`` the index in the sparse matrix (for non-orthogonal cases the last dimension is the overlap matrix) isc : ``int``, `None` the supercell index (or all) """ if isc is not None: raise NotImplementedError( "Requesting sub-Hamiltonian has not been implemented yet") return self._data.tocsr(index) def _Hk_unpolarized(self, k=(0, 0, 0), dtype=None): """ Return the Hamiltonian in a ``scipy.sparse.csr_matrix`` at `k`. Parameters ---------- k: ``array_like``, `[0,0,0]` k-point dtype : ``numpy.dtype`` default to `numpy.complex128` """ return self._Hk_polarized(k, dtype=dtype) def _Hk_polarized(self, k=(0, 0, 0), spin=0, dtype=None): """ Return the Hamiltonian in a ``scipy.sparse.csr_matrix`` at `k` for a polarized calculation Parameters ---------- k: ``array_like``, `[0,0,0]` k-point spin: ``int``, `0` the spin-index of the Hamiltonian dtype : ``numpy.dtype`` default to `numpy.complex128` """ if dtype is None: dtype = np.complex128 exp = np.exp dot = np.dot k = np.asarray(k, np.float64) k.shape = (-1, ) if not np.allclose(k, 0.): if np.dtype(dtype).kind != 'c': raise ValueError( "Hamiltonian setup at k different from Gamma requires a complex matrix" ) # Setup the Hamiltonian for this k-point Hf = self.tocsr(spin) no = self.no s = (no, no) H = csr_matrix(s, dtype=dtype) # Get the reciprocal lattice vectors dotted with k kr = dot(self.rcell, k) for si in range(self.sc.n_s): isc = self.sc_off[si, :] phase = exp(-1j * dot(kr, dot(self.cell, isc))) H += Hf[:, si * no:(si + 1) * no] * phase del Hf return H def _Hk_non_collinear(self, k=(0, 0, 0), dtype=None): """ Return the Hamiltonian in a ``scipy.sparse.csr_matrix`` at `k` for a non-collinear Hamiltonian. Parameters ---------- k: ``array_like``, `[0,0,0]` k-point dtype : ``numpy.dtype`` default to `numpy.complex128` """ if dtype is None: dtype = np.complex128 if np.dtype(dtype).kind != 'c': raise ValueError( "Non-collinear Hamiltonian setup requires a complex matrix") exp = np.exp dot = np.dot k = np.asarray(k, np.float64) k.shape = (-1, ) no = self.no * 2 s = (no, no) H = csr_matrix(s, dtype=dtype) # get back-dimension of the intrinsic sparse matrix no = self.no # Get the reciprocal lattice vectors dotted with k kr = dot(self.rcell, k) for si in range(self.sc.n_s): isc = self.sc_off[si, :] phase = exp(-1j * dot(kr, dot(self.cell, isc))) # diagonal elements Hf1 = self.tocsr(0)[:, si * no:(si + 1) * no] * phase for i, j, h in ispmatrixd(Hf1): H[i * 2, j * 2] += h Hf1 = self.tocsr(1)[:, si * no:(si + 1) * no] * phase for i, j, h in ispmatrixd(Hf1): H[1 + i * 2, 1 + j * 2] += h # off-diagonal elements Hf1 = self.tocsr(2)[:, si * no:(si + 1) * no] Hf2 = self.tocsr(3)[:, si * no:(si + 1) * no] # We expect Hf1 and Hf2 to be aligned equivalently! # TODO CHECK for i, j, hr in ispmatrixd(Hf1): # get value for the imaginary part hi = Hf2[i, j] H[i * 2, 1 + j * 2] += (hr - 1j * hi) * phase H[1 + i * 2, j * 2] += (hr + 1j * hi) * phase del Hf1, Hf2 return H def _Sk(self, k=(0, 0, 0), dtype=None): """ Return the Hamiltonian in a ``scipy.sparse.csr_matrix`` at `k`. Parameters ---------- k: ``array_like``, `[0,0,0]` k-point dtype : ``numpy.dtype`` default to `numpy.complex128` """ # we forward it to Hk_polarized (same thing for S) return self._Hk_polarized(k, spin=self.S_idx, dtype=dtype) def _Sk_non_collinear(self, k=(0, 0, 0), dtype=None): """ Return the Hamiltonian in a ``scipy.sparse.csr_matrix`` at `k`. Parameters ---------- k: ``array_like``, `[0,0,0]` k-point dtype : ``numpy.dtype`` default to `numpy.complex128` """ if dtype is None: dtype = np.complex128 if not np.allclose(k, 0.): if np.dtype(dtype).kind != 'c': raise ValueError( "Hamiltonian setup at k different from Gamma requires a complex matrix" ) exp = np.exp dot = np.dot k = np.asarray(k, np.float64) k.shape = (-1, ) # Get the overlap matrix Sf = self.tocsr(self.S_idx) no = self.no * 2 s = (no, no) S = csr_matrix(s, dtype=dtype) # Get back dimensionality of the intrinsic orbitals no = self.no # Get the reciprocal lattice vectors dotted with k kr = dot(self.rcell, k) for si in range(self.sc.n_s): isc = self.sc_off[si, :] phase = exp(-1j * dot(kr, dot(self.cell, isc))) # Setup the overlap for this k-point sf = Sf[:, si * no:(si + 1) * no] for i, j, s in ispmatrixd(sf): S[i * 2, j * 2] += s S[1 + i * 2, 1 + j * 2] += s del Sf return S def eigh(self, k=(0, 0, 0), atoms=None, eigvals_only=True, overwrite_a=True, overwrite_b=True, *args, **kwargs): """ Returns the eigenvalues of the Hamiltonian Setup the Hamiltonian and overlap matrix with respect to the given k-point, then reduce the space to the specified atoms and calculate the eigenvalues. All subsequent arguments gets passed directly to :code:`scipy.linalg.eigh` """ H = self.Hk(k=k) if not self.orthogonal: S = self.Sk(k=k) # Reduce sparsity pattern if not atoms is None: orbs = self.a2o(atoms) # Reduce space H = H[orbs, orbs] if not self.orthogonal: S = S[orbs, orbs] if self.orthogonal: return sli.eigh(H.todense(), *args, eigvals_only=eigvals_only, overwrite_a=overwrite_a, **kwargs) return sli.eigh(H.todense(), S.todense(), *args, eigvals_only=eigvals_only, overwrite_a=overwrite_a, overwrite_b=overwrite_b, **kwargs) def eigsh(self, k=(0, 0, 0), n=10, atoms=None, eigvals_only=True, *args, **kwargs): """ Returns the eigenvalues of the Hamiltonian Setup the Hamiltonian and overlap matrix with respect to the given k-point, then reduce the space to the specified atoms and calculate the eigenvalues. All subsequent arguments gets passed directly to :code:`scipy.linalg.eigh` """ # We always request the smallest eigenvalues... kwargs.update({'which': kwargs.get('which', 'SM')}) H = self.Hk(k=k) if not self.orthogonal: raise ValueError( "The sparsity pattern is non-orthogonal, you cannot use the Arnoldi procedure with scipy" ) # Reduce sparsity pattern if not atoms is None: orbs = self.a2o(atoms) # Reduce space H = H[orbs, orbs] return ssli.eigsh(H, k=n, *args, return_eigenvectors=not eigvals_only, **kwargs) def cut(self, seps, axis, *args, **kwargs): """ Cuts the tight-binding model into different parts. Creates a tight-binding model by retaining the parameters for the cut-out region, possibly creating a super-cell. Parameters ---------- seps : integer, optional number of times the structure will be cut. axis : integer the axis that will be cut """ new_w = None # Create new geometry with warnings.catch_warnings(record=True) as w: # Cause all warnings to always be triggered. warnings.simplefilter("always") # Create new cut geometry geom = self.geom.cut(seps, axis, *args, **kwargs) # Check whether the warning exists if len(w) > 0: if issubclass(w[-1].category, UserWarning): new_w = str(w[-1].message) new_w += ( "\n---\n" "The tight-binding model cannot be cut as the structure " "cannot be tiled accordingly. ANY use of the model has been " "relieved from sisl.") if new_w: warnings.warn(new_w, UserWarning) # Now we need to re-create the tight-binding model H = self.tocsr(0) has_S = self.S_idx > 0 if has_S: S = self.tocsr(self.S_idx) # they are created similarly, hence the following # should keep their order # First we need to figure out how long the interaction range is # in the cut-direction # We initialize to be the same as the parent direction nsc = np.copy(self.nsc) // 2 nsc[axis] = 0 # we count the new direction isc = np.zeros([3], np.int32) isc[axis] -= 1 out = False while not out: # Get supercell index isc[axis] += 1 try: idx = self.sc_index(isc) except: break # Figure out if the Hamiltonian has interactions # to 'isc' sub = H[0:geom.no, idx * self.no:(idx + 1) * self.no].indices[:] if has_S: sub = np.unique( np.concatenate( (sub, S[0:geom.no, idx * self.no:(idx + 1) * self.no].indices[:]), axis=0)) if len(sub) == 0: break c_max = np.amax(sub) # Count the number of cells it interacts with i = (c_max % self.no) // geom.no ic = idx * self.no for j in range(i): idx = ic + geom.no * j # We need to ensure that every "in between" index exists # if it does not we discard those indices if len( np.where( np.logical_and(idx <= sub, sub < idx + geom.no))[0]) == 0: i = j - 1 out = True break nsc[axis] = isc[axis] * seps + i if out: warnings.warn( 'Cut the connection at nsc={0} in direction {1}.'.format( nsc[axis], axis), UserWarning) # Update number of super-cells nsc[:] = nsc[:] * 2 + 1 geom.sc.set_nsc(nsc) # Now we have a correct geometry, and # we are now ready to create the sparsity pattern # Reduce the sparsity pattern, first create the new one ham = self.__class__(geom, nnzpr=np.amax(self._data.ncol), spin=self.spin, orthogonal=self.orthogonal) def sco2sco(M, o, m, seps, axis): # Converts an o from M to m isc = np.copy(M.o2isc(o)) isc[axis] = isc[axis] * seps # Correct for cell-offset isc[axis] = isc[axis] + (o % M.no) // m.no # find the equivalent cell in m try: # If a fail happens it is due to a discarded # interaction across a non-interacting region return (o % m.no, m.sc_index(isc) * m.no, m.sc_index(-isc) * m.no) except: return None, None, None # Copy elements if has_S: for jo in range(geom.no): # make smaller cut sH = H[jo, :] sS = S[jo, :] for io, iH in zip(sH.indices, sH.data): # Get the equivalent orbital in the smaller cell o, ofp, ofm = sco2sco(self.geom, io, ham.geom, seps, axis) if o is None: continue ham.H[jo, o + ofp] = iH ham.S[jo, o + ofp] = S[jo, io] ham.H[o, jo + ofm] = iH ham.S[o, jo + ofm] = S[jo, io] if np.any(sH.indices != sS.indices): # Ensure that S is also cut for io, iS in zip(sS.indices, sS.data): # Get the equivalent orbital in the smaller cell o, ofp, ofm = sco2sco(self.geom, io, ham.geom, seps, axis) if o is None: continue ham.H[jo, o + ofp] = H[jo, io] ham.S[jo, o + ofp] = iS ham.H[o, jo + ofm] = H[jo, io] ham.S[o, jo + ofm] = iS else: for jo in range(geom.no): sH = H[jo, :] for io, iH in zip(sH.indices, sH.data): # Get the equivalent orbital in the smaller cell o, ofp, ofm = sco2sco(self.geom, io, ham.geom, seps, axis) if o is None: continue ham[jo, o + ofp] = iH ham[o, jo + ofm] = iH return ham def tile(self, reps, axis): """ Returns a repeated tight-binding model for this, much like the `Geometry` The already existing tight-binding parameters are extrapolated to the new supercell by repeating them in blocks like the coordinates. Parameters ---------- reps : number of tiles (repetitions) axis : direction of tiling 0, 1, 2 according to the cell-direction """ # Create the new geometry g = self.geom.tile(reps, axis) raise NotImplementedError(('tiling a Hamiltonian model has not been ' 'fully implemented yet.')) def repeat(self, reps, axis): """ Refer to `tile` instead """ # Create the new geometry g = self.geom.repeat(reps, axis) raise NotImplementedError( ('repeating a Hamiltonian model has not been ' 'fully implemented yet, use tile instead.')) @classmethod def sp2HS(cls, geom, H, S=None): """ Returns a tight-binding model from a preset H, S and Geometry """ # Calculate number of connections nc = 0 has_S = not S is None # Ensure csr format H = H.tocsr() if has_S: S = S.tocsr() for i in range(geom.no): nc = max(nc, H[i, :].getnnz()) if has_S: nc = max(nc, S[i, :].getnnz()) # Create the Hamiltonian ham = cls(geom, nnzpr=nc, orthogonal=not has_S, dtype=H.dtype) # Copy data to the model if has_S: for jo, io in ispmatrix(H): ham.S[jo, io] = S[jo, io] # If the Hamiltonian for one reason or the other # is zero in the diagonal, then we *must* account for # this as it isn't captured in the above loop. skip_S = np.all(H.row == S.row) skip_S = skip_S and np.all(H.col == S.col) skip_S = False if not skip_S: # Re-convert back to allow index retrieval H = H.tocsr() for jo, io, s in ispmatrixd(S): ham[jo, io] = (H[jo, io], s) else: for jo, io, h in ispmatrixd(H): ham[jo, io] = h return ham @staticmethod def read(sile, *args, **kwargs): """ Reads Hamiltonian from `Sile` using `read_H`. Parameters ---------- sile : `Sile`, str a `Sile` object which will be used to read the Hamiltonian and the overlap matrix (if any) if it is a string it will create a new sile using `get_sile`. * : args passed directly to ``read_es(,**)`` """ # This only works because, they *must* # have been imported previously from sisl.io import get_sile, BaseSile if isinstance(sile, BaseSile): return sile.read_es(*args, **kwargs) else: return get_sile(sile).read_es(*args, **kwargs) def write(self, sile, *args, **kwargs): """ Writes a tight-binding model to the `Sile` as implemented in the :code:`Sile.write_es` method """ self.finalize() # This only works because, they *must* # have been imported previously from sisl.io import get_sile, BaseSile if isinstance(sile, BaseSile): sile.write_es(self, *args, **kwargs) else: get_sile(sile, 'w').write_es(self, *args, **kwargs) ############################### # Overload of math operations # ############################### def __add__(a, b): c = a.copy(dtype=get_dtype(b, other=a.dtype)) c += b return c __radd__ = __add__ def __iadd__(a, b): if isinstance(b, Hamiltonian): a._data += b._data else: a._data += b return a def __sub__(a, b): c = a.copy(dtype=get_dtype(b, other=a.dtype)) c -= b return c def __rsub__(a, b): if isinstance(b, Hamiltonian): c = b.copy(dtype=get_dtype(a, other=b.dtype)) c._data += -1 * a._data else: c = b + (-1) * a return c def __isub__(a, b): if isinstance(b, Hamiltonian): a._data -= b._data else: a._data -= b return a def __mul__(a, b): c = a.copy(dtype=get_dtype(b, other=a.dtype)) c *= b return c __rmul__ = __mul__ def __imul__(a, b): if isinstance(b, Hamiltonian): a._data *= b._data else: a._data *= b return a def __div__(a, b): c = a.copy(dtype=get_dtype(b, other=a.dtype)) c /= b return c def __rdiv__(a, b): c = b.copy(dtype=get_dtype(a, other=b.dtype)) c /= a return c def __idiv__(a, b): if isinstance(b, Hamiltonian): a._data /= b._data else: a._data /= b return a def __floordiv__(a, b): if isinstance(b, Hamiltonian): raise NotImplementedError c = a.copy(dtype=get_dtype(b, other=a.dtype)) c //= b return c def __ifloordiv__(a, b): if isinstance(b, Hamiltonian): raise NotImplementedError a._data //= b return a def __truediv__(a, b): if isinstance(b, Hamiltonian): raise NotImplementedError c = a.copy(dtype=get_dtype(b, other=a.dtype)) c /= b return c def __itruediv__(a, b): if isinstance(b, Hamiltonian): raise NotImplementedError a._data /= b return a def __pow__(a, b): c = a.copy(dtype=get_dtype(b, other=a.dtype)) c **= b return c def __rpow__(a, b): c = a.copy(dtype=get_dtype(b, other=a.dtype)) c._data = b**c._data return c def __ipow__(a, b): if isinstance(b, Hamiltonian): a._data **= b._data else: a._data **= b return a
class Hamiltonian(object): """ Hamiltonian object containing the coupling constants between orbitals. The Hamiltonian object contains information regarding the - geometry - coupling constants between orbitals It contains an intrinsic sparse matrix of the Hamiltonian elements. Assigning or changing Hamiltonian elements is as easy as with standard ``numpy`` assignments: >>> ham = Hamiltonian(...) >>> ham.H[1,2] = 0.1 which assigns 0.1 as the coupling constant between orbital 2 and 3. (remember that Python is 0-based elements). """ # The order of the Energy # I.e. whether energy should be in other units than Ry # This conversion is made: [eV] ** _E_order _E_order = 1 def __init__(self, geom, nnzpr=None, orthogonal=True, spin=1, dtype=None, *args, **kwargs): """Create tight-binding model from geometry Initializes a tight-binding model using the :code:`geom` object as the underlying geometry for the tight-binding parameters. """ self._geom = geom # Initialize the sparsity pattern self.reset(nnzpr=nnzpr, orthogonal=orthogonal, spin=spin, dtype=dtype) def reset(self, nnzpr=None, orthogonal=True, spin=1, dtype=None): """ The sparsity pattern is cleaned and every thing is reset. The object will be the same as if it had been initialized with the same geometry as it were created with. Parameters ---------- nnzpr: int number of non-zero elements per row orthogonal: boolean, True if there is an overlap matrix associated with the Hamiltonian spin: int, 1 number of spin-components dtype: ``numpy.dtype``, `numpy.float64` the datatype of the Hamiltonian """ # I know that this is not the most efficient way to # access a C-array, however, for constructing a # sparse pattern, it should be faster if memory elements # are closer... # Hence, this choice of having H and S like this # We check the first atom and its neighbours, we then # select max(5,len(nc) * 4) if nnzpr is None: nnzpr = self.geom.close(0) if nnzpr is None: nnzpr = [0,0] nnzpr = max(5, len(nnzpr) * 4) self._orthogonal = orthogonal # Reset the sparsity pattern if not orthogonal: self._data = SparseCSR((self.no, self.no_s, spin+1), nnzpr=nnzpr, dtype=dtype) else: self._data = SparseCSR((self.no, self.no_s, spin), nnzpr=nnzpr, dtype=dtype) self._spin = spin if spin == 1: self.UP = 0 self.DOWN = 0 self.S_idx = 1 elif spin == 2: self.UP = 0 self.DOWN = 1 self.S_idx = 2 else: raise ValueError("Currently the Hamiltonian has only been implemented with up to collinear spin.") if orthogonal: # There is no overlap matrix self.S_idx = -1 # Denote that one *must* specify all details of the elements self._def_dim = -1 def empty(self, keep=False): """ See `SparseCSR.empty` for specifics """ self._data.empty(keep) def copy(self, dtype=None): """ Return a copy of the `Hamiltonian` object """ if dtype is None: dtype = self.dtype H = self.__class__(self.geom, orthogonal=self.orthogonal, spin=self.spin, dtype=dtype) # Be sure to copy the content of the SparseCSR object H._data = self._data.copy(dtype=dtype) return H ######### Definitions of overrides ############ @property def geom(self): """ Return the attached geometry """ return self._geom @property def spin(self): """ Return number of spin-components in Hamiltonian """ return self._spin @property def dtype(self): """ Return data type of Hamiltonian (and overlap matrix) """ return self._data.dtype @property def orthogonal(self): """ Return whether the Hamiltonian is orthogonal """ return self._orthogonal def __len__(self): """ Returns number of rows in the Hamiltonian """ return self.geom.no def __repr__(self): """ Representation of the tight-binding model """ s = self.geom.__repr__() return s + '\nNumber of non-zero elements {0}'.format(self.nnz) def __getattr__(self, attr): """ Returns the attributes from the underlying geometry Any attribute not found in the tight-binding model will be looked up in the underlying geometry. """ return getattr(self.geom, attr) def __getitem__(self, key): """ Return Hamiltonian coupling elements for the index(s) """ dd = self._def_dim if dd >= 0: key = tuple(key) + (dd,) self._def_dim = -1 d = self._data[key] return d def __setitem__(self, key, val): """ Set or create couplings between orbitals in the Hamiltonian Override set item for slicing operations and enables easy setting of tight-binding parameters in a sparse matrix """ dd = self._def_dim if dd >= 0: key = tuple(key) + (dd,) self._def_dim = -1 self._data[key] = val def __get_H(self): self._def_dim = self.UP return self _get_H = __get_H def __set_H(self, key, value): if len(key) == 2: self._def_dim = self.UP self[key] = value _set_H = __set_H H = property(__get_H, __set_H) def __get_S(self): if self.orthogonal: return None self._def_dim = self.S_idx return self _get_S = __get_S def __set_S(self, key, value): if self.orthogonal: return None self._def_dim = self.S_idx self[key] = value _set_S = __set_S S = property(__get_S, __set_S) # Create iterations module def iter_linear(self): """ Iterations of the orbital space, two indices from loop An iterator returning the current atomic index and the corresponding orbital index. >>> for ia, io in self: In the above case `io` always belongs to atom `ia` and `ia` may be repeated according to the number of orbitals associated with the atom `ia`. """ for ia in self.geom: ia1, ia2 = self.geom.lasto[ia], self.geom.lasto[ia + 1] for io in range(ia1, ia2): yield ia, io __iter__ = iter_linear def construct(self, dR, param, eta=False): """ Automatically construct the Hamiltonian model based on ``dR`` and associated hopping integrals ``param``. Parameters ---------- dR : array_like radii parameters for tight-binding parameters. Must have same length as ``param`` or one less. If one less it will be extended with ``dR[0]/100`` param : array_like coupling constants corresponding to the ``dR`` ranges. ``param[0,:]`` are the tight-binding parameter for the all atoms within ``dR[0]`` of each atom. eta : `bool` (`False`) whether an ETA will be printed... """ # Ensure that we are dealing with a numpy array param = np.array(param) if len(dR) + 1 == len(param): R = np.hstack((dR[0] / 100, np.asarray(dR))) elif len(dR) == len(param): R = np.asarray(dR).copy() else: raise ValueError("Length of dR and param must be the same " "or dR one shorter than param. " "One tight-binding parameter for each radii.") if not self.orthogonal: if len(param[0]) != 2: raise ValueError("Number of parameters " "for each element is not 2. " "You must make len(param[0] == 2) for non-orthogonal Hamiltonians.") if np.any(np.diff(self.geom.lasto) > 1): warnings.warn("Automatically setting a tight-binding model " "for systems with atoms having more than 1 " "orbital is not adviced. Please do it your-self.") eq_atoms = [] def print_equal(eq_atoms): if len(eq_atoms) > 0: s = ("The geometry has one or more atoms having the same " "atomic position. " "The atoms are within {} Ang " "of each other.\n".format(R[0])) for ia, ja in eq_atoms: s += " {0:7d} -- {1:7d}\n".format(ia, ja) warnings.warn(s) def append_equal(eq_atoms, ia, idx): # Append to the list of equal atoms the atomic indices if len(idx) > 1: tmp = list(idx) # only add in "one" direction for ja in tmp: if ja > ia: eq_atoms.append( (ia,ja) ) if len(self.geom) < 1501: # there is no need to do anything complex # for small systems for ia in self.geom: # Find atoms close to 'ia' idx = self.geom.close(ia, dR=R) append_equal(eq_atoms, ia, idx[0]) for ix, h in zip(idx, param): # Set the tight-binding parameters self[ia, ix] = h print_equal(eq_atoms) return self # check how many atoms are within the standard 10 dR # range of some random atom. ia = np.random.randint(len(self.geom) - 1) # default block iterator d = self.geom.dR na = len(self.geom.close(ia, dR=d * 10)) # Convert to 1000 atoms spherical radii iR = int(4 / 3 * np.pi * d ** 3 / na * 1000) # Get number of atoms na = len(self.geom) na_run = 0 from time import time from sys import stdout t0 = time() # Do the loop for ias, idxs in self.geom.iter_block(iR=iR): # Loop the atoms inside for ia in ias: # Find atoms close to 'ia' idx = self.geom.close(ia, dR=R, idx=idxs) append_equal(eq_atoms, ia, idx[0]) for ix, h in zip(idx, param): # Set the tight-binding parameters self[ia, ix] = h if eta: na_run += len(ias) na -= len(ias) t1 = time() # calculate hours, minutes, seconds m, s = divmod( float(t1-t0)/na_run * na, 60) h, m = divmod(m, 60) stdout.write("Hamiltonian.construct() ETA = {0:d}h {1:d}m {2:.2f}s\r".format(int(h), int(m), s)) stdout.flush() print_equal(eq_atoms) @property def finalized(self): """ Whether the contained data is finalized and non-used elements have been removed """ return self._data.finalized def finalize(self): """ Finalizes the tight-binding model Finalizes the tight-binding model so that no new sparse elements can be added. Sparse elements can still be changed. """ self._data.finalize() # Get the folded Hamiltonian at the Gamma point Hk = self.Hk() nzs = Hk.nnz if nzs != (Hk + Hk.T).nnz: warnings.warn( 'Hamiltonian does not retain symmetric couplings, this might be problematic.') @property def nnz(self): """ Returns number of non-zero elements in the tight-binding model """ return self._data.nnz @property def no(self): """ Returns number of orbitals as used when the object was created """ return self._data.nr def tocsr(self, index): """ Return a ``scipy.sparse.csr_matrix`` from the specified index """ return self._data.tocsr(index) def Hk(self, k=(0, 0, 0), spin=0): """ Return the Hamiltonian in a ``scipy.sparse.csr_matrix`` at `k`. Parameters ---------- k: float*3 k-point spin: int, 0 the spin-index of the Hamiltonian """ # Create csr sparse formats. # We import here as the user might not want to # rely on this feature. from scipy.sparse import csr_matrix dot = np.dot k = np.asarray(k, np.float64) k.shape = (-1,) # Setup the Hamiltonian for this k-point Hf = self.tocsr(spin) no = self.no s = (no, no) H = csr_matrix(s, dtype=np.complex128) # Get the reciprocal lattice vectors dotted with k kr = dot(self.rcell, k) for si in range(self.sc.n_s): isc = self.sc_off[si, :] phase = np.exp(-1j * dot(kr, dot(self.cell, isc))) H += Hf[:, si * no:(si + 1) * no] * phase del Hf return H def Sk(self, k=(0, 0, 0), spin=0): """ Return the overlap matrix in a ``scipy.sparse.csr_matrix`` at `k`. Parameters ---------- k: float*3 k-point """ if self.orthogonal: return None # Create csr sparse formats. # We import here as the user might not want to # rely on this feature. from scipy.sparse import csr_matrix dot = np.dot k = np.asarray(k, np.float64) k.shape = (-1,) # Setup the Hamiltonian for this k-point Sf = self.tocsr(self.S_idx) no = self.no s = (no, no) S = csr_matrix(s, dtype=np.complex128) # Get the reciprocal lattice vectors dotted with k kr = dot(self.rcell, k) for si in range(self.sc.n_s): isc = self.sc_off[si, :] phase = np.exp(-1j * dot(kr, dot(self.cell, isc))) S += Sf[:, si * no:(si + 1) * no] * phase del Sf return S def eigh(self,k=(0,0,0), atoms=None, eigvals_only=True, overwrite_a=True, overwrite_b=True, *args, **kwargs): """ Returns the eigenvalues of the tight-binding model Setup the Hamiltonian and overlap matrix with respect to the given k-point, then reduce the space to the specified atoms and calculate the eigenvalues. All subsequent arguments gets passed directly to :code:`scipy.linalg.eigh` """ H = self.Hk(k=k) if not self.orthogonal: S = self.Sk(k=k) # Reduce sparsity pattern if not atoms is None: orbs = self.a2o(atoms) # Reduce space H = H[orbs, orbs] if not self.orthogonal: S = S[orbs, orbs] if self.orthogonal: return sli.eigh(H.todense(), *args, eigvals_only=eigvals_only, overwrite_a=overwrite_a, **kwargs) return sli.eigh(H.todense(), S.todense(), *args, eigvals_only=eigvals_only, overwrite_a=overwrite_a, overwrite_b=overwrite_b, **kwargs) def eigsh(self, k=(0,0,0), n=10, atoms=None, eigvals_only=True, *args, **kwargs): """ Returns the eigenvalues of the tight-binding model Setup the Hamiltonian and overlap matrix with respect to the given k-point, then reduce the space to the specified atoms and calculate the eigenvalues. All subsequent arguments gets passed directly to :code:`scipy.linalg.eigh` """ # We always request the smallest eigenvalues... kwargs.update({'which':kwargs.get('which', 'SM')}) H = self.Hk(k=k) if not self.orthogonal: raise ValueError("The sparsity pattern is non-orthogonal, you cannot use the Arnoldi procedure with scipy") # Reduce sparsity pattern if not atoms is None: orbs = self.a2o(atoms) # Reduce space H = H[orbs, orbs] return ssli.eigsh(H, k=n, *args, return_eigenvectors=not eigvals_only, **kwargs) def cut(self, seps, axis, *args, **kwargs): """ Cuts the tight-binding model into different parts. Creates a tight-binding model by retaining the parameters for the cut-out region, possibly creating a super-cell. Parameters ---------- seps : integer, optional number of times the structure will be cut. axis : integer the axis that will be cut """ new_w = None # Create new geometry with warnings.catch_warnings(record=True) as w: # Cause all warnings to always be triggered. warnings.simplefilter("always") # Create new cut geometry geom = self.geom.cut(seps, axis, *args, **kwargs) # Check whether the warning exists if len(w) > 0: if issubclass(w[-1].category, UserWarning): new_w = str(w[-1].message) new_w += ("\n---\n" "The tight-binding model cannot be cut as the structure " "cannot be tiled accordingly. ANY use of the model has been " "relieved from sisl.") if new_w: warnings.warn(new_w, UserWarning) # Now we need to re-create the tight-binding model H = self.Hk() S = self.Sk() # they are created similarly, hence the following # should keep their order # First we need to figure out how long the interaction range is # in the cut-direction # We initialize to be the same as the parent direction nsc = np.copy(self.nsc) // 2 nsc[axis] = 0 # we count the new direction isc = np.zeros([3], np.int32) isc[axis] -= 1 out = False while not out: # Get supercell index isc[axis] += 1 try: idx = self.sc_index(isc) except: break # Figure out if the Hamiltonian has interactions # to 'isc' sub = H[0:geom.no, idx * self.no:(idx + 1) * self.no].indices[:] sub = np.unique(np.hstack( (sub, S[0:geom.no, idx * self.no:(idx + 1) * self.no].indices[:]))) if len(sub) == 0: break c_max = np.amax(sub) # Count the number of cells it interacts with i = (c_max % self.no) // geom.no ic = idx * self.no for j in range(i): idx = ic + geom.no * j # We need to ensure that every "in between" index exists # if it does not we discard those indices if len(np.where( np.logical_and(idx <= sub, sub < idx + geom.no) )[0]) == 0: i = j - 1 out = True break nsc[axis] = isc[axis] * seps + i if out: warnings.warn( 'Cut the connection at nsc={0} in direction {1}.'.format( nsc[axis], axis), UserWarning) # Update number of super-cells nsc[:] = nsc[:] * 2 + 1 geom.sc.set_nsc(nsc) # Now we have a correct geometry, and # we are now ready to create the sparsity pattern # Reduce the sparsity pattern, first create the new one ham = self.__class__(geom, nc=np.amax(self.ncol), spin=self.spin) def sco2sco(M, o, m, seps, axis): # Converts an o from M to m isc = np.copy(M.o2isc(o)) isc[axis] = isc[axis] * seps # Correct for cell-offset isc[axis] = isc[axis] + (o % M.no) // m.no # find the equivalent cell in m try: # If a fail happens it is due to a discarded # interaction across a non-interacting region return (o % m.no, m.sc_index(isc) * m.no, m.sc_index(-isc) * m.no) except: return None, None, None # Copy elements for jo in range(geom.no): # make smaller cut sH = H[jo, :] sS = S[jo, :] for io, iH in zip(sH.indices, sH.data): # Get the equivalent orbital in the smaller cell o, ofp, ofm = sco2sco(self.geom, io, ham.geom, seps, axis) if o is None: continue ham[jo, o + ofp] = iH, S[jo, io] ham[o, jo + ofm] = iH, S[jo, io] if np.any(sH.indices != sS.indices): # Ensure that S is also cut for io, iS in zip(sS.indices, sS.data): # Get the equivalent orbital in the smaller cell o, ofp, ofm = sco2sco(self.geom, io, ham.geom, seps, axis) if o is None: continue ham[jo, o + ofp] = H[jo, io], iS ham[o, jo + ofm] = H[jo, io], iS return ham def tile(self, reps, axis): """ Returns a repeated tight-binding model for this, much like the `Geometry` The already existing tight-binding parameters are extrapolated to the new supercell by repeating them in blocks like the coordinates. Parameters ---------- reps : number of tiles (repetitions) axis : direction of tiling 0, 1, 2 according to the cell-direction """ # Create the new geometry g = self.geom.tile(reps, axis) raise NotImplementedError(('tiling a Hamiltonian model has not been ' 'fully implemented yet.')) def repeat(self, reps, axis): """ Refer to `tile` instead """ # Create the new geometry g = self.geom.repeat(reps, axis) raise NotImplementedError(('repeating a Hamiltonian model has not been ' 'fully implemented yet, use tile instead.')) @classmethod def sp2HS(cls, geom, H, S=None): """ Returns a tight-binding model from a preset H, S and Geometry """ # Calculate number of connections nc = 0 has_S = not S is None # Ensure csr format H = H.tocsr() if has_S: S = S.tocsr() for i in range(geom.no): nc = max(nc, H[i, :].getnnz()) if has_S: nc = max(nc, S[i, :].getnnz()) if has_S: ham = cls(geom, nnzpr=nc, orthogonal=False, dtype=H.dtype) else: ham = cls(geom, nnzpr=nc, dtype=H.dtype) # Copy data to the model H = H.tocoo() if has_S: for jo, io, h in zip(H.row, H.col, H.data): ham[jo, io] = (h, S[jo, io]) # Convert S to coo matrix S = S.tocoo() # If the Hamiltonian for one reason or the other # is zero in the diagonal, then we *must* account for # this as it isn't captured in the above loop. skip_S = np.all(H.row == S.row) skip_S = skip_S and np.all(H.col == S.col) if not skip_S: # Re-convert back to allow index retrieval H = H.tocsr() for jo, io, s in zip(S.row, S.col, S.data): ham[jo, io] = (H[jo, io], s) else: for jo, io, h in zip(H.row, H.col, H.data): ham[jo, io] = h return ham @staticmethod def read(sile, *args, **kwargs): """ Reads Hamiltonian from `Sile` using `read_H`. Parameters ---------- sile : `Sile`, str a `Sile` object which will be used to read the Hamiltonian and the overlap matrix (if any) if it is a string it will create a new sile using `get_sile`. * : args passed directly to ``read_es(,**)`` """ # This only works because, they *must* # have been imported previously from sisl.io import get_sile, BaseSile if isinstance(sile, BaseSile): return sile.read_es(*args, **kwargs) else: return get_sile(sile).read_es(*args, **kwargs) def write(self, sile, *args, **kwargs): """ Writes a tight-binding model to the `Sile` as implemented in the :code:`Sile.write_es` method """ self.finalize() # This only works because, they *must* # have been imported previously from sisl.io import get_sile, BaseSile if isinstance(sile, BaseSile): sile.write_es(self, *args, **kwargs) else: get_sile(sile, 'w').write_es(self, *args, **kwargs) ############################### # Overload of math operations # ############################### def __add__(a, b): if isinstance(b, Hamiltonian): raise NotImplementedError c = a.copy(dtype=get_dtype(b, other=a.dtype)) c += b return c __radd__ = __add__ def __iadd__(a, b): if isinstance(b, Hamiltonian): raise NotImplementedError a._data += b return a def __sub__(a, b): if isinstance(b, Hamiltonian): raise NotImplementedError c = a.copy(dtype=get_dtype(b, other=a.dtype)) c -= b return c def __rsub__(a, b): if isinstance(b, Hamiltonian): raise NotImplementedError c = b + (-1) * a return c def __isub__(a, b): if isinstance(b, Hamiltonian): raise NotImplementedError a._data -= b return a def __mul__(a, b): if isinstance(b, Hamiltonian): raise NotImplementedError c = a.copy(dtype=get_dtype(b, other=a.dtype)) c *= b return c __rmul__ = __mul__ def __imul__(a, b): if isinstance(b, Hamiltonian): raise NotImplementedError a._data *= b return a def __div__(a, b): if isinstance(a, Hamiltonian): if isinstance(b, Hamiltonian): raise NotImplementedError c = a.copy(dtype=get_dtype(b, other=a.dtype)) c /= b elif isinstance(b, Hamiltonian): c = b.copy(dtype=get_dtype(a, other=b.dtype)) c._data = a / c._data return c def __idiv__(a, b): if isinstance(b, Hamiltonian): raise NotImplementedError a._data /= b return a def __floordiv__(a, b): if isinstance(b, Hamiltonian): raise NotImplementedError c = a.copy(dtype=get_dtype(b, other=a.dtype)) c //= b return c def __ifloordiv__(a, b): if isinstance(b, Hamiltonian): raise NotImplementedError a._data //= b return a def __truediv__(a, b): if isinstance(b, Hamiltonian): raise NotImplementedError c = a.copy(dtype=get_dtype(b, other=a.dtype)) c /= b return c def __itruediv__(a, b): if isinstance(b, Hamiltonian): raise NotImplementedError a._data /= b return a def __pow__(a, b): if isinstance(b, Hamiltonian): raise NotImplementedError c = a.copy(dtype=get_dtype(b, other=a.dtype)) c **= b return c def __rpow__(a, b): if isinstance(b, SparseCSR): raise NotImplementedError c = a.copy(dtype=get_dtype(b, other=a.dtype)) c._data = b ** c._data return c def __ipow__(a, b): if isinstance(b, Hamiltonian): raise NotImplementedError a._data **= b return a
class TestSparseCSR(object): def setUp(self): self.s1 = SparseCSR((10,100), dtype=np.int32) self.s2 = SparseCSR((10,100,2)) def test_init1(self): assert_equal(self.s1.dtype, np.int32) assert_equal(self.s2.dtype, np.float64) assert_true(np.allclose(self.s1.data, self.s1.data)) assert_true(np.allclose(self.s2.data, self.s2.data)) def test_init2(self): SparseCSR((10,100)) for d in [np.int32, np.float64, np.complex128]: s = SparseCSR((10,100), dtype=d) assert_equal(s.shape, (10, 100, 1)) assert_equal(s.dim, 1) assert_equal(s.dtype, d) for k in [1, 2]: s = SparseCSR((10,100,k), dtype=d) assert_equal(s.shape, (10, 100, k)) assert_equal(s.dim, k) s = SparseCSR((10,100), dim=k, dtype=d) assert_equal(s.shape, (10, 100, k)) assert_equal(s.dim, k) s = SparseCSR((10,100, 3), dim=k, dtype=d) assert_equal(s.shape, (10, 100, 3)) assert_equal(s.dim, 3) def test_init3(self): csr = sc.sparse.csr_matrix( (10,10), dtype=np.int32) csr[0, 1] = 1 csr[0, 2] = 2 sp = SparseCSR(csr) assert_equal(sp.dtype, np.int32) assert_equal(sp.shape, (10, 10, 1)) assert_equal(len(sp), 2) assert_equal(sp[0, 1], 1) assert_equal(sp[0, 2], 2) sp = SparseCSR(csr, dtype=np.float64) assert_equal(sp.shape, (10, 10, 1)) assert_equal(sp.dtype, np.float64) assert_equal(len(sp), 2) assert_equal(sp[0, 1], 1) assert_equal(sp[0, 2], 2) def test_init4(self): csr = sc.sparse.csr_matrix( (10,10), dtype=np.int32) csr[0, 1] = 1 csr[0, 2] = 2 print(csr.indices, csr.indptr) sp = SparseCSR((csr.data, csr.indices, csr.indptr)) assert_equal(sp.dtype, np.int32) assert_equal(sp.shape, (10, 10, 1)) assert_equal(len(sp), 2) assert_equal(sp[0, 1], 1) assert_equal(sp[0, 2], 2) sp = SparseCSR((csr.data, csr.indices, csr.indptr), dtype=np.float64) assert_equal(sp.shape, (10, 10, 1)) assert_equal(sp.dtype, np.float64) assert_equal(len(sp), 2) assert_equal(sp[0, 1], 1) assert_equal(sp[0, 2], 2) def test_create1(self): self.s1[0,[1,2,3]] = 1 assert_equal(self.s1.nnz, 3) self.s1[2,[1,2,3]] = 1 assert_equal(self.s1.nnz, 6) self.s1.empty(keep=True) assert_equal(self.s1.nnz, 6) self.s1.empty() assert_equal(self.s1.nnz, 0) def test_create2(self): for i in range(10): j = range(i*4, i*4+3) self.s1[0, j] = i assert_equal(len(self.s1), (i+1)*3) for jj in j: assert_equal(self.s1[0, jj], i) assert_equal(self.s1[1, jj], 0) self.s1.empty() def test_create3(self): for i in range(10): j = range(i*4, i*4+3) self.s1[0, j] = i assert_equal(len(self.s1), (i+1)*3) self.s1[0, range((i+1)*4, (i+1)*4+3)] = None assert_equal(len(self.s1), (i+1)*3) for jj in j: assert_equal(self.s1[0, jj], i) assert_equal(self.s1[1, jj], 0) self.s1.empty() def test_finalize1(self): self.s1[0,[1,2,3]] = 1 self.s1[2,[1,2,3]] = 1. assert_false(self.s1.finalized) self.s1.finalize() assert_true(self.s1.finalized) self.s1.empty(keep=True) assert_true(self.s1.finalized) self.s1.empty() assert_false(self.s1.finalized) def test_delitem1(self): self.s1[0,[1,2,3]] = 1 assert_equal(len(self.s1), 3) del self.s1[0,1] assert_equal(len(self.s1), 2) assert_equal(self.s1[0,1], 0) assert_equal(self.s1[0,2], 1) assert_equal(self.s1[0,3], 1) self.s1[0,[1,2,3]] = 1 del self.s1[0,[1,3]] assert_equal(len(self.s1), 1) assert_equal(self.s1[0,1], 0) assert_equal(self.s1[0,2], 1) assert_equal(self.s1[0,3], 0) self.s1.empty() def test_op1(self): for i in range(10): j = range(i*4, i*4+3) self.s1[0, j] = i # i+ self.s1 += 1 for jj in j: assert_equal(self.s1[0, jj], i+1) assert_equal(self.s1[1, jj], 0) # i- self.s1 -= 1 for jj in j: assert_equal(self.s1[0, jj], i) assert_equal(self.s1[1, jj], 0) # i* self.s1 *= 2 for jj in j: assert_equal(self.s1[0, jj], i*2) assert_equal(self.s1[1, jj], 0) # // self.s1 //= 2 for jj in j: assert_equal(self.s1[0, jj], i) assert_equal(self.s1[1, jj], 0) # i** self.s1 **= 2 for jj in j: assert_equal(self.s1[0, jj], i**2) assert_equal(self.s1[1, jj], 0) def test_op2(self): for i in range(10): j = range(i*4, i*4+3) self.s1[0, j] = i # + s = self.s1 + 1 for jj in j: assert_equal(s[0, jj], i+1) assert_equal(self.s1[0, jj], i) assert_equal(s[1, jj], 0) # - s = self.s1 - 1 for jj in j: assert_equal(s[0, jj], i-1) assert_equal(self.s1[0, jj], i) assert_equal(s[1, jj], 0) # - (r) s = 1 - self.s1 for jj in j: assert_equal(s[0, jj], 1 - i) assert_equal(self.s1[0, jj], i) assert_equal(s[1, jj], 0) # * s = self.s1 * 2 for jj in j: assert_equal(s[0, jj], i*2) assert_equal(self.s1[0, jj], i) assert_equal(s[1, jj], 0) # // s = s // 2 for jj in j: assert_equal(s[0, jj], i) assert_equal(self.s1[0, jj], i) assert_equal(s[1, jj], 0) # ** s = self.s1 ** 2 for jj in j: assert_equal(s[0, jj], i**2) assert_equal(self.s1[0, jj], i) assert_equal(s[1, jj], 0) # ** (r) s = 2 ** self.s1 for jj in j: assert_equal(s[0, jj], 2 ** self.s1[0, jj]) assert_equal(self.s1[0, jj], i) assert_equal(s[1, jj], 0) def test_op3(self): S = SparseCSR((10,100), dtype=np.int32) # Create initial stuff for i in range(10): j = range(i*4, i*4+3) S[0, j] = i for op in ['add', 'sub', 'mul', 'pow']: func = getattr(S, '__{}__'.format(op)) s = func(1) assert_equal(s.dtype, np.int32) s = func(1.) assert_equal(s.dtype, np.float64) if op != 'pow': s = func(1.j) assert_equal(s.dtype, np.complex128) S = S.copy(dtype=np.float64) for op in ['add', 'sub', 'mul', 'pow']: func = getattr(S, '__{}__'.format(op)) s = func(1) assert_equal(s.dtype, np.float64) s = func(1.) assert_equal(s.dtype, np.float64) if op != 'pow': s = func(1.j) assert_equal(s.dtype, np.complex128) S = S.copy(dtype=np.complex128) for op in ['add', 'sub', 'mul', 'pow']: func = getattr(S, '__{}__'.format(op)) s = func(1) assert_equal(s.dtype, np.complex128) s = func(1.) assert_equal(s.dtype, np.complex128) if op != 'pow': s = func(1.j) assert_equal(s.dtype, np.complex128) def test_op4(self): S = SparseCSR((10,100), dtype=np.int32) # Create initial stuff for i in range(10): j = range(i*4, i*4+3) S[0, j] = i s = 1 + S assert_equal(s.dtype, np.int32) s = 1. + S assert_equal(s.dtype, np.float64) s = 1.j + S assert_equal(s.dtype, np.complex128) s = 1 - S assert_equal(s.dtype, np.int32) s = 1. - S assert_equal(s.dtype, np.float64) s = 1.j - S assert_equal(s.dtype, np.complex128) s = 1 * S assert_equal(s.dtype, np.int32) s = 1. * S assert_equal(s.dtype, np.float64) s = 1.j * S assert_equal(s.dtype, np.complex128) s = 1 ** S assert_equal(s.dtype, np.int32) s = 1. ** S assert_equal(s.dtype, np.float64) s = 1.j ** S assert_equal(s.dtype, np.complex128)