diff --git a/kwant/physics/dispersion.py b/kwant/physics/dispersion.py index f4e0e42ed52a78c58fb9f168de1f4976138c955a..e3618796ced0fa35909553843a90b51f6372dae7 100644 --- a/kwant/physics/dispersion.py +++ b/kwant/physics/dispersion.py @@ -38,7 +38,7 @@ class Bands: (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. If `derivative_order > 0` or `return_eigenvectors = True`, - additional arrays are returned returned. + additional arrays are returned. Examples -------- @@ -48,6 +48,7 @@ class Bands: >>> pyplot.plot(momenta, energies) >>> pyplot.show() """ + _crossover_size = 8 def __init__(self, sys, args=(), *, params=None): syst = sys @@ -60,45 +61,36 @@ class Bands: self.hop[:, : hop.shape[1]] = hop self.hop[:, hop.shape[1]:] = 0 - # 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. - self._crossover_size = 8 - def __call__(self, k, derivative_order=0, return_eigenvectors=False): - """Calculate all energies :math:`E` (optionally higher derivatives - :math:`E', E''` and eigenvectors) for a given momentum :math:`k` + 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 - + :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 + Maximal derivative order to calculate. Default is zero. return_eigenvectors : bool, optional - if set to `True` return the eigenvectors as last tuple element - + 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 : numpy float array, size ``nbands`` energies :math:`E` - velocities : numpy float array, size `nbands` + velocities : numpy float array, size ``nbands`` velocities (first order energy derivatives :math:`E'`) - curvatures : numpy float array, size `nbands` + curvatures : numpy float array, size ``nbands`` curvatures (second energy derivatives :math:`E''`) - eigenvectors : numpy float array, size `nbands` + eigenvectors : numpy float array, size ``nbands x nbands`` eigenvectors The number of retured elements varies. If `derivative_order > 0` @@ -110,11 +102,9 @@ class Bands: Notes ----- - * The ordering of the energies and velocities is the same and - according to the magnitude of the energy eigenvalues. - Therefore, the bands are not continously ordered. - * The curvature `E''` can be only calculated - for non-degenerate bands. + * 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 @@ -125,37 +115,46 @@ class Bands: mat = self.hop * complex(math.cos(k), -math.sin(k)) h0 = mat + mat.conjugate().transpose() + self.ham - if derivative_order == 0: - if return_eigenvectors: - energies, eigenvectors = np.linalg.eigh(h0) - return energies, eigenvectors - return np.sort(np.linalg.eigvalsh(h0).real) - - energies, eigenvectors = np.linalg.eigh(h0) - h1 = 1j*(- mat + mat.conjugate().transpose()) - if derivative_order == 1 and len(energies) > self._crossover_size: - velocities = np.sum(eigenvectors.conjugate() * (h1 @ eigenvectors), - axis=0).real + + # compute energies and if required eigenvectors + if return_eigenvectors or derivative_order > 0: + energies, eigenvectors = np.linalg.eigh(h0) else: - ph1p = eigenvectors.conjugate().transpose() @ h1 @ eigenvectors - velocities = np.diag(ph1p).real - if derivative_order == 1: - if return_eigenvectors: - return energies, velocities, eigenvectors - return energies, velocities - - # 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, out=np.zeros_like(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 - if derivative_order == 2: - if return_eigenvectors: - return energies, velocities, curvatures, eigenvectors - return energies, velocities, curvatures - raise NotImplementedError('derivative_order= {} not implemented' - .format(derivative_order)) + energies = np.sort(np.linalg.eigvalsh(h0).real) + output = (energies,) + + 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, out=np.zeros_like(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 d1a6d9a08bdb2543ac1a32f836fb4e023a7c5996..a42d6deadf5d88bb0390318cb5442f88229742dc 100644 --- a/kwant/physics/tests/test_dispersion.py +++ b/kwant/physics/tests/test_dispersion.py @@ -97,10 +97,10 @@ def test_band_velocity_derivative(): assert_array_almost_equal(dvel, num_dvel) -def test_eigenvector_calculation(k=1.2, lead=make_lead()): # k-point arbitrary - - lead = lead.finalized() +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) @@ -144,7 +144,7 @@ def test_eigenvector_calculation(k=1.2, lead=make_lead()): # k-point arbitrary assert_array_almost_equal(v_1, v_2) -def test_raise_implemented(k=1, lead=make_lead()): # k-point arbitrary - bands = kwant.physics.Bands(lead.finalized()) - raises(NotImplementedError, bands, k, derivative_order=-1) +def test_raise_implemented(): + k = 1 # k-point arbitrary + bands = kwant.physics.Bands(make_lead().finalized()) raises(NotImplementedError, bands, k, derivative_order=3)