diff --git a/kwant/physics/dispersion.py b/kwant/physics/dispersion.py index 9934ad71a8f2ea236c09e31b239094adf5a994ce..5c98bf2618536fea4262df0ef8476dcc9181e721 100644 --- a/kwant/physics/dispersion.py +++ b/kwant/physics/dispersion.py @@ -6,6 +6,8 @@ # the file AUTHORS.rst at the top-level directory of this distribution and at # http://kwant-project.org/authors. +"""Band structure calculation for the leads""" + import math import numpy as np from .. import system @@ -35,7 +37,8 @@ class Bands: An instance of this class can be called like a function. Given a momentum (currently this must be a scalar as all infinite systems are quasi-1-d), it returns a NumPy array containing the eigenenergies of all modes at this - momentum + momentum. If derivative_order > 0 or return_eigenvectors = True, + additional arrays are returned. Examples -------- @@ -45,6 +48,7 @@ class Bands: >>> pyplot.plot(momenta, energies) >>> pyplot.show() """ + _crossover_size = 8 def __init__(self, sys, args=(), *, params=None): syst = sys @@ -57,9 +61,100 @@ class Bands: self.hop[:, : hop.shape[1]] = hop self.hop[:, hop.shape[1]:] = 0 - def __call__(self, k): - # Note: Equation to solve is - # (V^\dagger e^{ik} + H + V e^{-ik}) \psi = E \psi + def __call__(self, k, derivative_order=0, return_eigenvectors=False): + r"""Calculate band energies at a given momentum. + + :math:E_n, \quad E'_n = dE_n / dk, \quad E''_n = d^2E_n / dk^2, + \quad n \in \{0, nbands - 1\} + + :math:nbands is the number of open modes. + Eigenvectors are orthonormal. + + Parameters + ---------- + k : float + momentum + derivative_order : {0, 1, 2}, optional + Maximal derivative order to calculate. Default is zero. + return_eigenvectors : bool, optional + if set to True return the eigenvectors as last tuple element. + By default, no eigenvectors are returned. + + Returns + ------- + energies : numpy float array, size nbands + energies :math:E + velocities : numpy float array, size nbands + velocities (first order energy derivatives :math:E') + curvatures : numpy float array, size nbands + curvatures (second energy derivatives :math:E'') + eigenvectors : numpy float array, size nbands x nbands + eigenvectors + + The number of returned elements varies. If derivative_order > 0 + we return all derivatives up to the order derivative_order, + and total the number of returned elements is derivative_order + 1 + If return_eigenvectors is True, in addition the eigenvectors are + returned as the last element. In that case, the total number of + returned elements is derivative_order + 2. + + Notes + ----- + * All the output arrays are sorted from the lowest energy band + to the highest. + * The curvature E'' can be only calculated for non-degenerate bands. + """ + # Equation to solve is + # (V^\dagger e^{ik} + H + V e^{-ik}) \psi = E \psi + # we obtain the derivatives by perturbing around momentum k + # The perturbed Hamiltonian H(k) is + # H(k+dk) = H_0(k) + H_1(k) dk + H_2(k) dk^2 + O(dk^3) + # and we use h0, h1 and h2 in the code to refer to above terms + mat = self.hop * complex(math.cos(k), -math.sin(k)) - mat += mat.conjugate().transpose() + self.ham - return np.sort(np.linalg.eigvalsh(mat).real) + h0 = mat + mat.conjugate().transpose() + self.ham + + # compute energies and if required eigenvectors + # numpy routines eigh and eigvalsh return eigenvalues in ascending order + if return_eigenvectors or derivative_order > 0: + energies, eigenvectors = np.linalg.eigh(h0) + else: + energies = np.linalg.eigvalsh(h0) + output = (energies.real,) + + if derivative_order >= 1: # compute velocities + h1 = 1j*(- mat + mat.conjugate().transpose()) + # Switch computation from diag(a^\dagger @ b) to sum(a.T * b, axis=0), + # which is computationally more efficient for larger matrices. + # a and b are NxN matrices, and we switch for N > _crossover_size. + # Both ways are numerically (but not necessarily bitwise) equivalent. + if derivative_order == 1 and len(energies) > self._crossover_size: + velocities = np.sum(eigenvectors.conjugate() * (h1 @ eigenvectors), + axis=0).real + else: + ph1p = eigenvectors.conjugate().transpose() @ h1 @ eigenvectors + velocities = np.diag(ph1p).real + output += (velocities,) + + if derivative_order >= 2: # compute curvatures + # ediff_{i,j} = 1 / (E_i - E_j) if i != j, else 0 + ediff = energies.reshape((-1, 1)) - energies.reshape((1, -1)) + ediff = np.divide(1, ediff, where=(ediff != 0)) + h2 = - (mat + mat.conjugate().transpose()) + curvatures = np.sum(eigenvectors.conjugate() * (h2 @ eigenvectors) + - 2 * ediff * np.abs(ph1p)**2, axis=0).real + # above expression is similar to: curvatures = + # np.diag(eigenvectors.conjugate().transpose() @ h2 @ eigenvectors + # + 2 * ediff @ np.abs(ph1p)**2).real + output += (curvatures,) + + if derivative_order > 2: + raise NotImplementedError('Derivatives of the energy dispersion ' + + 'only implemented up to second order.') + if return_eigenvectors: + output += (eigenvectors,) + if len(output) == 1: + # Backwards compatibility: if returning only energies, + # don't make it a length-1 tuple. + return output[0] + return output diff --git a/kwant/physics/tests/test_dispersion.py b/kwant/physics/tests/test_dispersion.py index 414ac17854a8e491c8010900686f938b5f35d5ac..a42d6deadf5d88bb0390318cb5442f88229742dc 100644 --- a/kwant/physics/tests/test_dispersion.py +++ b/kwant/physics/tests/test_dispersion.py @@ -6,26 +6,33 @@ # the file AUTHORS.rst at the top-level directory of this distribution and at # http://kwant-project.org/authors. +import numpy as np from numpy.testing import assert_array_almost_equal, assert_almost_equal from pytest import raises +from numpy import linspace import kwant from math import pi, cos, sin -def test_band_energies(N=5): + +def make_lead(): syst = kwant.Builder(kwant.TranslationalSymmetry((-1, 0))) lat = kwant.lattice.square() syst[[lat(0, 0), lat(0, 1)]] = 3 syst[lat(0, 1), lat(0, 0)] = -1 syst[((lat(1, y), lat(0, y)) for y in range(2))] = -1 + return syst + - band_energies = kwant.physics.Bands(syst.finalized()) +def test_band_energies(N=5): + band_energies = kwant.physics.Bands(make_lead().finalized()) for i in range(-N, N): k = i * pi / N energies = band_energies(k) assert_array_almost_equal(sorted(energies), sorted([2 - 2 * cos(k), 4 - 2 * cos(k)])) + def test_same_as_lead(): syst = kwant.Builder(kwant.TranslationalSymmetry((-1,))) lat = kwant.lattice.chain() @@ -39,6 +46,7 @@ def test_same_as_lead(): for momentum in momenta: assert_almost_equal(bands(momentum)[0], 0) + def test_raise_nonhermitian(): syst = kwant.Builder(kwant.TranslationalSymmetry((-1,))) lat = kwant.lattice.chain() @@ -46,3 +54,97 @@ def test_raise_nonhermitian(): syst[lat(0), lat(1)] = complex(cos(0.2), sin(0.2)) syst = syst.finalized() raises(ValueError, kwant.physics.Bands, syst) + + +def test_band_velocities(): + syst = kwant.Builder(kwant.TranslationalSymmetry((-1, 0))) + lat = kwant.lattice.square() + syst[lat(0, 0)] = 1 + syst[lat(0, 1)] = 3 + syst[lat(1, 0), lat(0, 0)] = -1 + syst[lat(1, 1), lat(0, 1)] = 2 + bands = kwant.physics.Bands(syst.finalized()) + eps = 1E-4 + for k in linspace(-pi, pi, 200): + vel = bands(k, derivative_order=1)[1] + # higher order formula for first derivative to get required accuracy + num_vel = (- bands(k+2*eps) + bands(k-2*eps) + + 8*(bands(k+eps) - bands(k-eps))) / (12 * eps) + assert_array_almost_equal(vel, num_vel) + + +def test_band_velocity_derivative(): + syst = kwant.Builder(kwant.TranslationalSymmetry((-1, 0))) + lat = kwant.lattice.square() + syst[lat(0, 0)] = 1 + syst[lat(0, 1)] = 3 + syst[lat(1, 0), lat(0, 0)] = -1 + syst[lat(1, 1), lat(0, 1)] = 2 + bands = kwant.physics.Bands(syst.finalized()) + eps = 1E-4 + eps2 = eps * eps + c3 = 1 / 90 + c2 = - 3 / 20 + c1 = 3 / 2 + c0 = - 49 / 18 + for k in linspace(-pi, pi, 200): + dvel = bands(k, derivative_order=2)[2] + # higher order formula for second derivative to get required accuracy + num_dvel = (c3 * (bands(k+3*eps) + bands(k-3*eps)) + + c2 * (bands(k+2*eps) + bands(k-2*eps)) + + c1 * (bands(k+eps) + bands(k-eps)) + + c0 * bands(k)) / eps2 + assert_array_almost_equal(dvel, num_dvel) + + +def test_eigenvector_calculation(): + lead = make_lead().finalized() + bands = kwant.physics.Bands(lead) + k = 1.2 # k-point arbitrary + + # check if eigenvalues are sorted and always sorted in the same way + energies = bands(k) + assert (np.sort(energies) == energies).all() + e_1 = bands(k, derivative_order=0) + e_2, *_ = bands(k, derivative_order=0, return_eigenvectors=True) + assert_array_almost_equal(energies, e_1) + assert_array_almost_equal(energies, e_2) + for order in [1, 2]: + e_1, *_ = bands(k, derivative_order=order) + e_2, *_ = bands(k, derivative_order=order, return_eigenvectors=True) + assert_array_almost_equal(energies, e_1) + assert_array_almost_equal(energies, e_2) + + # check eigenvector + unity_matrix = np.identity(len(energies)) + zero_matrix = 0 * unity_matrix + for order in [0, 1, 2]: + eval_evec = bands(k, derivative_order=order, return_eigenvectors=True) + + assert len(eval_evec) == order + 2 + energies, *_, psi = eval_evec + + # completeness relation + outer_product = psi @ psi.conjugate().transpose() + assert_array_almost_equal(outer_product.real, unity_matrix) + assert_array_almost_equal(outer_product.imag, zero_matrix) + + # eigenvalue equation + mat = bands.hop * np.exp(- 1j * k) + hamiltonian = mat + mat.conjugate().transpose() + bands.ham + energies = np.asarray([energies]) + assert_array_almost_equal(hamiltonian @ psi, energies * psi) + + # check that algorithm switch is correct + bands_switch = kwant.physics.Bands(lead) + bands_switch._crossover_size = 0 + for order in [1, 2]: + e_1, v_1, *_ = bands(k, order) + e_2, v_2, *_ = bands_switch(k, order) + assert_array_almost_equal(v_1, v_2) + + +def test_raise_implemented(): + k = 1 # k-point arbitrary + bands = kwant.physics.Bands(make_lead().finalized()) + raises(NotImplementedError, bands, k, derivative_order=3)