The Non-Hermitian Skin Effect#

Hatano and Nelson (1996) studied a chain whose hoppings to the right and to the left differ, \((H\psi)_n = t_R\psi_{n+1} + t_L\psi_{n-1}\) – a non-reciprocal, non-Hermitian lattice. Twenty years later it became the paradigm of a phenomenon with no Hermitian counterpart, the skin effect (Yao and Wang; Kunst, Edvardsson, Budich and Bergholtz, 2018):

  • on a ring, the spectrum \(E(k) = t_Re^{ik} + t_Le^{-ik}\) is an ellipse, winding once around every energy inside it (spectral winding number \(W = \pm1\));

  • on an open chain it collapses onto a real segment, \(|E| < 2\sqrt{t_Rt_L}\), and every eigenstate piles up at one end – the bulk becomes a skin.

spectral_winding() and finite_ham() compute both. The open-chain bands live on a generalized Brillouin zone, see plot_generalized_brillouin_zone.py.

import numpy as np
import matplotlib.pyplot as plt

import tbkit.lattices as lattices
from tbkit.bridges import finite_system
from tbkit.kspace import KSpace
from tbkit.plot import Plot

t_right, t_left = 1., 0.5
hn = KSpace(lattices.chain())
hn.set_hopping([{'i': 0, 'j': 0, 'R': (1,), 't': t_right},
                        {'i': 0, 'j': 0, 'R': (-1,), 't': t_left}], hermitian=False)

The lattice#

Ten sites of the Hatano-Nelson chain (finite_system()): one site per cell and nearest-neighbour bonds, non-reciprocal – each carries \(t_R\) one way and \(t_L\) the other. The widths drawn are those of \(t_R\).

fig_lat = Plot(finite_system(hn, 10)).lattice(plt_hop=True, ms=12, figsize=(7, 1.2))
plot skin effect

Ring and open chain#

n = 60
ring = np.linalg.eigvals(hn.finite_ham(n, periodic=True))
en, vec = np.linalg.eig(hn.finite_ham(n))
# compared as sets: E(k = pi) is real, and its angle (+pi or -pi) depends on
# the sign of a roundoff imaginary part
expected = [t_right*np.exp(1j*k) + t_left*np.exp(-1j*k) for k in 2*np.pi*np.arange(n)/n]
assert np.allclose(np.sort_complex(np.round(ring, 8)), np.sort_complex(np.round(expected, 8)))
assert np.max(np.abs(en.imag)) < 1e-8 and np.max(np.abs(en.real)) < 2*np.sqrt(t_right*t_left)
winding = hn.spectral_winding(0.)
print('Spectral winding about E = 0: {:.3f}; open-chain spectrum real, within +-{:.3f}.'
          .format(winding, 2*np.sqrt(t_right*t_left)))
assert np.isclose(winding, 1.)

# every eigenstate on the left end
weight = np.abs(vec) ** 2 / np.sum(np.abs(vec) ** 2, axis=0)
left = weight[:n // 4].sum(axis=0)
print('Weight on the leftmost quarter: at least {:.0%} for every eigenstate.'.format(left.min()))
assert left.min() > 0.9

fig, axes = plt.subplots(1, 2, figsize=(10, 4))
axes[0].plot(ring.real, ring.imag, 'ob', ms=3, label='ring')
axes[0].plot(en.real, en.imag, 'or', ms=3, label='open chain')
axes[0].set_xlabel(r'Re $E$')
axes[0].set_ylabel(r'Im $E$')
axes[0].legend()
axes[0].set_aspect('equal')
axes[1].semilogy(np.arange(n), weight[:, np.argsort(en.real)[::10]], lw=1)
axes[1].set_xlabel('site')
axes[1].set_ylabel(r'$|\psi_n|^2$')
axes[1].set_title('open chain: every state on the left end')
fig.set_layout_engine('tight')
open chain: every state on the left end
Spectral winding about E = 0: 1.000; open-chain spectrum real, within +-1.414.
Weight on the leftmost quarter: at least 100% for every eigenstate.

Total running time of the script: (0 minutes 0.088 seconds)

Gallery generated by Sphinx-Gallery