We are going to:
Parts of this tutorial are based on the online course on topology in condensed matter
In [ ]:
# We'll have 3D plotting and 2D band structure, so we need a handful of helper functions.
%run matplotlib_setup.ipy
from types import SimpleNamespace
from ipywidgets import interact
import matplotlib
from matplotlib import pyplot
from mpl_toolkits import mplot3d
import numpy as np
import kwant
from wraparound import wraparound
def momentum_to_lattice(k):
"""Transform momentum to the basis of reciprocal lattice vectors.
See https://en.wikipedia.org/wiki/Reciprocal_lattice#Generalization_of_a_dual_lattice
"""
B = np.array(graphene.prim_vecs).T
A = B.dot(np.linalg.inv(B.T.dot(B)))
return np.linalg.solve(A, k)
def dispersion_2D(syst, args=None, lim=1.5*np.pi, num_points=200):
"""A simple plot of 2D band structure."""
if args is None:
args = []
momenta = np.linspace(-lim, lim, num_points)
energies = []
for kx in momenta:
for ky in momenta:
lattice_k = momentum_to_lattice([kx, ky])
h = syst.hamiltonian_submatrix(args=(list(args) + list(lattice_k)))
energies.append(np.linalg.eigvalsh(h))
energies = np.array(energies).reshape(num_points, num_points, -1)
emin, emax = np.min(energies), np.max(energies)
kx, ky = np.meshgrid(momenta, momenta)
fig = pyplot.figure()
axes = fig.add_subplot(1, 1, 1, projection='3d')
for band in range(energies.shape[-1]):
axes.plot_surface(kx, ky, energies[:, :, band], cstride=2, rstride=2,
cmap=matplotlib.cm.RdBu_r, vmin=emin, vmax=emax,
linewidth=0.1)
Quantum Hall effect creates protected edge states using a strong magnetic field. Another way to create those is to start from a system with Dirac cones, and open gaps in those.
There is a real (and a very important) two-dimensional system which has Dirac cones: graphene. So in this chapter we will take graphene and make it into a topological system with chiral edge states.
Graphene is a single layer of carbon atoms arranged in a honeycomb lattice. It is a triangular lattice with two atoms per unit cell, type $A$ and type $B$, represented by red and blue sites in the figure:
In [ ]:
graphene = kwant.lattice.general([[1, 0], [1/2, np.sqrt(3)/2]], # lattice vectors
[[0, 0], [0, 1/np.sqrt(3)]]) # Coordinates of the sites
a, b = graphene.sublattices
We now create a Builder
with the translational symmetries of graphene, and calculate the bulk dispersion of graphene.
Hence, the wave function in a unit cell can be written as a vector $(\Psi_A, \Psi_B)^T$ of amplitudes on the two sites $A$ and $B$. Taking a simple tight-binding model where electrons can hop between neighboring sites with hopping strength $t$, one obtains the Bloch Hamiltonian:
$$ H_0(\mathbf{k})= \begin{pmatrix} 0 & h(\mathbf{k}) \\ h^\dagger(\mathbf{k}) & 0 \end{pmatrix}\,, $$with $\mathbf{k}=(k_x, k_y)$ and
$$h(\mathbf{k}) = t_1\,\sum_i\,\exp\,\left(i\,\mathbf{k}\cdot\mathbf{a}_i\right)\,.$$Here $\mathbf{a}_i$ are the three vectors in the figure, connecting nearest neighbors of the lattice [we set the lattice spacing to one, so that for instance $\mathbf{a}_1=(1,0)$]. Introducing a set of Pauli matrices $\sigma$ which act on the sublattice degree of freedom, we can write the Hamiltonian in a compact form as
$$H_0(\mathbf{k}) = t_1\,\sum_i\,\left[\sigma_x\,\cos(\mathbf{k}\cdot\mathbf{a}_i)-\sigma_y \,\sin(\mathbf{k}\cdot\mathbf{a}_i)\right]\,.$$The energy spectrum $E(\mathbf{k}) = \pm \,\left|h(\mathbf{k})\right|$ gives rise to the famous band structure of graphene, with the two bands touching at the six corners of the Brillouin zone:
In [ ]:
bulk_graphene = kwant.Builder(kwant.TranslationalSymmetry(*graphene.prim_vecs))
bulk_graphene[graphene.shape((lambda pos: True), (0, 0))] = 0
bulk_graphene[graphene.neighbors(1)] = 1
dispersion_2D(wraparound(bulk_graphene).finalized())
Let's also create 1D ribbons of graphene.
There are two nontrivial directions: armchair and zigzag
In [ ]:
zigzag_ribbon = kwant.Builder(kwant.TranslationalSymmetry([1, 0]))
zigzag_ribbon[graphene.shape((lambda pos: abs(pos[1]) < 9), (0, 0))] = 0
zigzag_ribbon[graphene.neighbors(1)] = 1
kwant.plotter.bands(zigzag_ribbon.finalized());
In [ ]:
# Solution
armchair_ribbon = kwant.Builder(kwant.TranslationalSymmetry([0, np.sqrt(3)]))
armchair_ribbon[graphene.shape((lambda pos: abs(pos[0]) < 9), (0, 0))] = 0
armchair_ribbon[graphene.neighbors(1)] = 1
kwant.plotter.bands(armchair_ribbon.finalized(), fig_size=(12, 8));
In [ ]:
# Solution
zigzag_ribbon = kwant.Builder(kwant.TranslationalSymmetry([1, 0]))
zigzag_ribbon[a.shape((lambda pos: abs(pos[1]) < 9), (0, 0))] = 0.2
zigzag_ribbon[b.shape((lambda pos: abs(pos[1]) < 9), (0, 0))] = -0.2
zigzag_ribbon[graphene.neighbors(1)] = 1
kwant.plotter.bands(zigzag_ribbon.finalized(), fig_size=(12, 8));
We have now opened a gap, but there are no protected states inside it.
The more interesting way to open the gap in graphene dispersion is introduced by Duncan Haldane, Phys. Rev. Lett. 61, 2015 (1988)
The idea of this model is to break inversion symmetry that protects the Dirac points by adding next-nearest neighbor hoppings
In [ ]:
nnn_hoppings_a = (((-1, 0), a, a), ((0, 1), a, a), ((1, -1), a, a))
nnn_hoppings_b = (((1, 0), b, b), ((0, -1), b, b), ((-1, 1), b, b))
nnn_hoppings = nnn_hoppings_a + nnn_hoppings_b
In [ ]:
def nnn_hopping(site1, site2, params):
return 1j * params.t_2
def onsite(site, params):
return params.m * (1 if site.family == a else -1)
def add_hoppings(syst):
syst[graphene.neighbors(1)] = 1
syst[[kwant.builder.HoppingKind(*hopping) for hopping in nnn_hoppings]] = nnn_hopping
haldane = kwant.Builder(kwant.TranslationalSymmetry(*graphene.prim_vecs))
haldane[graphene.shape((lambda pos: True), (0, 0))] = onsite
haldane[graphene.neighbors(1)] = 1
haldane[[kwant.builder.HoppingKind(*hopping) for hopping in nnn_hoppings]] = nnn_hopping
@interact(t_2=(0, .08, .01))
def qshe_dispersion(t_2=0, m=.2):
dispersion_2D(wraparound(haldane).finalized(), [SimpleNamespace(t_2=t_2, m=m)], num_points=100)
Now we see that the gap closes in one of the Dirac cones, and does not close in the other half. Let's see what this means for the dispersion relation in a ribbon.
In [ ]:
# Solution
def nnn_hopping(site1, site2, params):
return 1j * params.t_2
def onsite(site, params):
return params.m * (1 if site.family == a else -1)
def add_hoppings(syst):
syst[graphene.neighbors(1)] = 1
syst[[kwant.builder.HoppingKind(*hopping) for hopping in nnn_hoppings]] = nnn_hopping
zigzag_haldane = kwant.Builder(kwant.TranslationalSymmetry([1, 0]))
zigzag_haldane[graphene.shape((lambda pos: abs(pos[1]) < 9), (0, 0))] = onsite
zigzag_haldane[graphene.neighbors(1)] = 1
zigzag_haldane[[kwant.builder.HoppingKind(*hopping) for hopping in nnn_hoppings]] = nnn_hopping
@interact(t_2=(0, .08, .01))
def qshe_dispersion(t_2=0, m=.2):
kwant.plotter.bands(zigzag_haldane.finalized(), [SimpleNamespace(t_2=t_2, m=m)])
(Following: C.L. Kane and E.J. Mele, Phys. Rev. Lett. 95, 226801 (2005))
Haldane model breaks time-reversal symmetry and inversion symmetry. Lattice-scale hoppings that break time-reversal symmetry do not appear in non-magnetic materials. We can make the Hamiltonian invariant under inversion and time-reversal by making the next-nearest neighbor hoppings spin-dependent.
So if we take those hoppings equal to $i t_2 \sigma_z$, we get teh
In [ ]:
# Pauli matrices
s0 = np.identity(2)
sx = np.array([[0, 1], [1, 0]])
sy = np.array([[0, -1j], [1j, 0]])
sz = np.diag([1, -1])
In [ ]:
def spin_orbit(site1, site2, params):
return 1j * params.t_2 * sz
def onsite(site, params):
return s0 * params.m * (1 if site.family == a else -1)
def add_hoppings(syst):
syst[graphene.neighbors(1)] = s0
syst[[kwant.builder.HoppingKind(*hopping) for hopping in nnn_hoppings]] = spin_orbit
In [ ]:
bulk_kane_mele = kwant.Builder(kwant.TranslationalSymmetry(*graphene.prim_vecs))
bulk_kane_mele[graphene.shape((lambda pos: True), (0, 0))] = onsite
add_hoppings(bulk_kane_mele)
@interact(t_2=(0, .3, .01))
def qshe_dispersion(t_2=0, m=.2):
dispersion_2D(wraparound(bulk_kane_mele).finalized(), [SimpleNamespace(t_2=t_2, m=m)], num_points=100)
In [ ]:
zigzag_kane_mele = kwant.Builder(kwant.TranslationalSymmetry([1, 0]))
zigzag_kane_mele[graphene.shape((lambda pos: abs(pos[1]) < 9), (0, 0))] = onsite
add_hoppings(zigzag_kane_mele)
@interact(t_2=(0, .12, .01))
def qshe_zigzag_dispersion(t_2=0, m=.2):
kwant.plotter.bands(zigzag_kane_mele.finalized(), [SimpleNamespace(t_2=t_2, m=m)])
We have an open important question: what protects these new edge states? Is it spin conservation? If we don't break the conservation of $\sigma_z$, then it is obvious that the edge states don't disappear (we have two copies of Haldane model after all).
The most interesting property of the quantum spin Hall effect is that it does not rely on any conservation law. The reason for this is Kramers degeneracy, that prevents two states that are time-reversal partners of each other from coupling to each other.
As a final test of topological protection, let's add an extra parameter that breaks spin conservation and check what happens to the edge states.
In [ ]:
# Solution
zigzag_kane_mele = kwant.Builder(kwant.TranslationalSymmetry([1, 0]))
zigzag_kane_mele[graphene.shape((lambda pos: abs(pos[1]) < 9), (0, 0))] = onsite
zigzag_kane_mele[graphene.neighbors(1)] = s0
zigzag_kane_mele[kwant.builder.HoppingKind((0, 0), b, a)] = s0 + 0.3j * sx
zigzag_kane_mele[[kwant.builder.HoppingKind(*hopping) for hopping in nnn_hoppings]] = spin_orbit
@interact(t_2=(0, .12, .01))
def qshe_zigzag_dispersion(t_2=0, m=.2):
kwant.plotter.bands(zigzag_kane_mele.finalized(), [SimpleNamespace(t_2=t_2, m=m)])