Source code for tbkit.bridges

r'''
Bridges between the two pipelines of tbkit: the real-space *System* (a
finite lattice, its sparse Hamiltonian) and the reciprocal-space *KSpace*
(the Bloch Hamiltonian :math:`H(\mathbf{k})` of the infinite lattice).

* *kspace_from_system* reads the Bloch model off a translation-invariant
  *System* built the real-space way (*get_lattice*, *set_hopping* by
  neighbour order, *set_onsite*, ...).
* *finite_model* and *finite_system* cut a finite sample (open boundaries,
  or a torus) out of a *KSpace* model: its sparse Hamiltonian with the site
  positions, or a ready-to-use *System*.

Both use the convention of *KSpace.finite_ham*: the hopping
:math:`t_{ij}(\mathbf{R}) = \langle i,\mathbf{0}|H|j,\mathbf{R}\rangle`
joins orbital *i* of every cell to orbital *j* of the cell :math:`\mathbf{R}`
further. So, for the same model, the spectrum of a System on an
:math:`N_1\times N_2` torus is the set of KSpace bands on the
:math:`N_1\times N_2` Brillouin-zone mesh.
'''
from __future__ import annotations

import numpy as np
from numpy.typing import NDArray
import scipy.sparse as sp

import tbkit.error_handling as error_handling
import tbkit.neighbours as neighbours
from tbkit.kspace import KSpace
from tbkit.lattice import Lattice
from tbkit.orbital import OrbitalSystem
from tbkit.system import System, HOP_DTYPE, ATOL, _upper


def _coords(lat: Lattice) -> NDArray[np.float64]:
    '''
    Private function. Site coordinates of a finite lattice, shape (sites, space_dim).
    '''
    return np.stack([lat.coor[f] for f in ('x', 'y', 'z')[:lat.space_dim]], axis=1)


[docs] def cell_orbitals(lat: Lattice) -> tuple[NDArray[np.int64], NDArray[np.int64]]: r''' Get, for every site of a finite lattice (*lat.coor*), the unit-cell orbital it is a copy of and its cell: site *s* sits at :math:`\boldsymbol\tau_{o_s} + \sum_k n_{s,k}\mathbf{a}_k`, with the tag of orbital :math:`o_s` (within ``ATOL``). Parameters ---------- lat **Lattice** instance, after *get_lattice* (sites may have been removed, but not moved). Returns ------- cells : Integer ndarray, shape (sites, dim) :math:`n_{s,k}`. orbitals : Integer ndarray, shape (sites,) :math:`o_s`. ''' error_handling.lat(lat) error_handling.empty_coor(lat.coor) a, tau, tags = neighbours._geometry(lat) pos = _coords(lat) pinv = np.linalg.pinv(a) match = np.zeros((lat.sites, len(tau)), bool) cells = np.zeros((lat.sites, len(tau), len(a)), int) for o in range(len(tau)): rel = pos - tau[o] n = np.rint(rel @ pinv).astype(int) match[:, o] = (np.linalg.norm(n @ a - rel, axis=1) < ATOL) & (lat.coor['tag'] == tags[o]) cells[:, o] = n error_handling.lattice_sites(match.sum(axis=1)) orb = np.argmax(match, axis=1) return cells[np.arange(lat.sites), orb], orb
[docs] def kspace_from_system(sys: System, periodic: bool | None = None, tol: float = 1e-9) -> KSpace: r''' Get the Bloch Hamiltonian of a periodic model defined the real-space way: every matrix element :math:`H_{ab}` of the finite *System* (built by *get_ham*) joins orbital :math:`o_a` of cell :math:`\mathbf{n}_a` to orbital :math:`o_b` of cell :math:`\mathbf{n}_b` (see *cell_orbitals*), i.e. is the hopping :math:`t_{o_ao_b}(\mathbf{n}_b-\mathbf{n}_a)` of the infinite lattice. Each such hopping must take the same value in every cell where it appears (bonds cut by the boundary are simply missing). The lattice must be large enough to hold every bond of the model. A System on a torus (*periodic*, e.g. from *finite_system*) has bonds that wrap around: their lattice vectors are then taken modulo the :math:`N_1\times N_2\times\dots` cells of *get_lattice*, as the shortest image (so the model's range must stay below half the torus). A Hermitian hopping matrix gives a Hermitian model (one representative per bond, *set_hopping* adding the conjugate); otherwise every matrix element is kept, with ``hermitian=False``. Onsite energies are taken orbital by orbital. Parameters ---------- sys : **System** instance with its hoppings set (one orbital per site) periodic : Boolean Default value None: True if *sys* has periodic boundaries along some primitive vector (``System(lat, periodic=...)``, or *finite_system* with ``periodic=True``). The System is a torus of the *lat.n1* x *lat.n2* (x *lat.n3*) cells of *get_lattice*. tol : Positive real number, default 1e-9 Largest allowed difference between equivalent matrix elements. Returns ------- ks **KSpace** instance (spinless), on a new **Lattice** with the *unit_cell* and *prim_vec* of *sys.lat*. Examples -------- Example usage:: lat = lattices.honeycomb() lat.get_lattice(6, 6) sys = System(lat) sys.set_hopping([{'n': 1, 't': -1.}, {'n': 2, 't': 0.1}]) gra = kspace_from_system(sys) # the Bloch model of the same graphene ''' error_handling.sys(sys) error_handling.not_orbital_system(sys, OrbitalSystem) if periodic is None: periodic = any(sys.periodic) error_handling.boolean(periodic, 'periodic') error_handling.positive_real(tol, 'tol') sys.get_ham() cells, orb = cell_orbitals(sys.lat) ham = sys.ham.tocoo() rows, cols, vals = ham.row, ham.col, ham.data.astype('c16') R = cells[cols] - cells[rows] if periodic: sizes = np.array((sys.lat.n1, sys.lat.n2, sys.lat.n3)[:R.shape[1]]) R = (R + sizes // 2) % sizes - sizes // 2 # shortest image, in [-N/2, N/2) error_handling.torus_range(R, sizes) keys = np.column_stack([orb[rows], orb[cols], R]) uniq, first, inv = np.unique(keys, axis=0, return_index=True, return_inverse=True) inv = inv.ravel() values = vals[first] error_handling.translation_invariant(float(np.abs(vals - values[inv]).max()), tol) off = ham - sp.diags(ham.diagonal()) hermitian = bool(abs(off - off.conj().T).max() <= tol) lat = Lattice(unit_cell=list(sys.lat.unit_cell), prim_vec=list(sys.lat.prim_vec)) ks = KSpace(lat) list_hop = [] for key, t in zip(uniq, values): i, j, R = int(key[0]), int(key[1]), tuple(int(n) for n in key[2:]) if i == j and not any(R): ks.onsite[i] = t continue nonzero = [n for n in R if n != 0] if hermitian and not ((nonzero and nonzero[0] > 0) or (not nonzero and i < j)): continue # the conjugate of a kept representative list_hop.append({'i': i, 'j': j, 'R': R, 't': complex(t)}) ks.set_hopping(list_hop, hermitian=hermitian) return ks
def _finite_sites(ks: KSpace, n_cells: tuple[int, ...]) -> tuple[NDArray[np.float64], NDArray]: ''' Private function. Positions and tags of the rows of *ks.finite_ham(n_cells)*. ''' n_tot = int(np.prod(n_cells)) cells = np.array(np.unravel_index(np.arange(n_tot), n_cells, order='F')).T a = np.array(ks.lat.prim_vec, dtype='f8') pos = (cells @ a)[:, None, :] + ks.orbital_positions()[None, :, :] tags = np.repeat(ks.tags, 2) if ks.spin else ks.tags return pos.reshape(n_tot * ks.norb, ks.space_dim), np.tile(tags, n_tot)
[docs] def finite_model( ks: KSpace, n_cells: int | tuple[int, ...], periodic: bool = False, ) -> tuple[sp.csr_matrix, NDArray[np.float64], NDArray]: r''' Get a finite sample of a *KSpace* model: its sparse Hamiltonian (that of *KSpace.finite_ham*, open or periodic boundaries), with the position and tag of every row. Spinful models are supported (the two spin components of a site share its position). Parameters ---------- ks : **KSpace** instance n_cells : Positive integer, or tuple of *dim* positive integers Number of unit cells along each primitive vector. periodic : Boolean, default False Periodic boundaries (a torus). Returns ------- ham : Complex CSR matrix, shape (N*norb, N*norb) positions : Real ndarray, shape (N*norb, space_dim) Row :math:`c\,\mathrm{norb}+o` sits at :math:`\boldsymbol\tau_o + \sum_k n_k\mathbf{a}_k` (cell *c* has :math:`c = n_1 + N_1 n_2 + N_1N_2 n_3`). tags : String ndarray, shape (N*norb,) Sublattice tags. ''' error_handling.kspace(ks, KSpace) error_handling.nk(n_cells, ks.dim) error_handling.boolean(periodic, 'periodic') n_cells = (n_cells,) * ks.dim if isinstance(n_cells, int) else n_cells pos, tags = _finite_sites(ks, n_cells) return ks.finite_ham(n_cells, periodic=periodic, sparse=True), pos, tags
[docs] def finite_system(ks: KSpace, n_cells: int | tuple[int, ...], periodic: bool = False) -> System: r''' Build a finite *System* out of a (spinless) *KSpace* model: *lat.coor* holds the sites of :math:`N_1\times N_2\times\dots` unit cells (in the order of *finite_model*), and *sys.hop* / *sys.onsite* the model's hoppings and onsite energies, so that *sys.get_ham()* gives *ks.finite_ham(n_cells, periodic)*. Every real-space tool (*get_eig*, *get_ldos*, *Plot*, disorder, ...) then applies. In *sys.hop*, each bond is stored with the conventions of *System.set_hopping*: a Hermitian model keeps one orientation per bond (angle in [0, 180), *get_ham* adds the conjugate); a non-reciprocal one keeps both, the missing direction with a zero amplitude. 'n' is the neighbour order of the bond (0 for coinciding orbitals), 'tag' the sublattice pair. On a torus, a bond that wraps around keeps its short bond vector for its angle, and a hopping that wraps onto its own site is added to the onsite energy; the System has periodic boundaries (``sys.periodic``), so that further *set_hopping* calls, *Plot* and *get_bott_index* treat it as a torus. Parameters ---------- ks : **KSpace** instance, with ``spin=False`` n_cells : Positive integer, or tuple of *dim* positive integers periodic : Boolean, default False Periodic boundaries (a torus). Returns ------- sys : **System** instance, with its hoppings set Examples -------- Example usage:: sys = finite_system(gra, (30, 30), periodic=True) sys.get_ham() sys.get_eig() # = gra.mesh_bands((30, 30)), sorted ''' error_handling.kspace(ks, KSpace) error_handling.nk(n_cells, ks.dim) error_handling.boolean(periodic, 'periodic') error_handling.spinless(ks.spin) n_cells = (n_cells,) * ks.dim if isinstance(n_cells, int) else n_cells pos, tags = _finite_sites(ks, n_cells) lat = Lattice(unit_cell=list(ks.lat.unit_cell), prim_vec=list(ks.lat.prim_vec)) coor = np.zeros(len(pos), dtype=lat.dtype) for c, f in enumerate(('x', 'y', 'z')[:ks.space_dim]): coor[f] = pos[:, c] coor['tag'] = tags lat.coor, lat.sites = coor, len(coor) lat.n1, lat.n2, lat.n3 = (tuple(n_cells) + (1, 1))[:3] sys = System(lat, periodic=periodic) _, n_tot, entries = ks._finite_entries(n_cells, periodic) tau = ks.orbital_positions() rows = np.concatenate([np.zeros(0, int)] + [e[0] for e in entries]) cols = np.concatenate([np.zeros(0, int)] + [e[1] for e in entries]) t = np.concatenate([np.zeros(0, 'c16')] + [np.full(len(e[0]), e[6], 'c16') for e in entries]) d = np.concatenate([np.zeros((0, ks.space_dim))] + [np.repeat((e[5] + tau[e[3]] - tau[e[2]])[None], len(e[0]), axis=0) for e in entries]) n_sites = len(pos) diag = rows == cols sys.onsite = np.tile(ks.onsite, n_tot) + (np.bincount(rows[diag], t[diag].real, n_sites) + 1j * np.bincount(rows[diag], t[diag].imag, n_sites)) rows, cols, t, d = rows[~diag], cols[~diag], t[~diag], d[~diag] dist = np.linalg.norm(d, axis=1) zero = dist <= ATOL ang = neighbours._bond_angles(d) up = np.where(zero, rows < cols, _upper(ang)) ang_up = np.where(zero, 0., np.maximum(np.where(up, ang, neighbours._bond_angles(-d)), 0.)) if ks._nonreciprocal: i = np.concatenate([rows, cols]) j = np.concatenate([cols, rows]) t = np.concatenate([t, np.zeros(len(t), 'c16')]) ang = np.concatenate([np.where(up, ang_up, ang_up - 180.), np.where(up, ang_up - 180., ang_up)]) dist = np.concatenate([dist, dist]) else: i, j, t, ang, dist = rows[up], cols[up], t[up], ang_up[up], dist[up] hop = np.zeros(len(i), dtype=HOP_DTYPE) hop['n'] = neighbours.shell_index(ks.lat, dist) hop['i'], hop['j'], hop['t'], hop['ang'] = i, j, t, ang hop['tag'] = np.char.add(tags[i], tags[j]) sys.hop = hop return sys