Edge modes in a Finite SSH chain#

We sweep the Su–Schrieffer–Heeger (SSH) model from the trivial to the topological phase, tracking boundary states in a finite chain and relating them to the bulk Berry phase.

What you will learn

  • Build a parameterised SSH TBModel and visualise its unit cell.

  • Cut a finite chain and monitor how eigenvalues/eigenvectors evolve with hopping ratios.

  • Analyze edge localization from wavefunction amplitudes.

  • Compare bulk polarization via Berry phases computed with WFArray.

from pythtb import TBModel, WFArray, Mesh, Lattice
import matplotlib.pyplot as plt
import numpy as np

Model generator#

ssh(v, w) returns the periodic two-site unit cell with intracell hopping v and intercell hopping w.

Hint

pythtb.models.ssh exposes the same model function defined here.

def ssh(v, w):
    lat_vecs = [[1]]
    orb_vecs = [[-1 / 4], [1 / 4]]
    lat = Lattice(lat_vecs, orb_vecs, periodic_dirs=[0])
    my_model = TBModel(lat)

    my_model.set_hop(v, 0, 1, [0])
    my_model.set_hop(w, 1, 0, [1])

    return my_model
model = ssh(v=1, w=1 / 2)
model.visualize()
(<Figure size 800x800 with 1 Axes>, <Axes: xlabel='x', ylabel='y'>)
../_images/69571a7c700df5cb3842b988f7b9cd6c7c1da33e6dd461c4622098b4ed3c2ec2.png

Finite chain sweep#

For each intercell hopping \(w\) we cut a chain of n_cells unit cells, diagonalize it, and stash both eigenvalues and eigenvectors to analyse edge behaviour.

t = 1
n_cells = 30
n_delta = 101
delta_values = np.linspace(-1, 1, n_delta)

evals_d = []
evecs_d = []
for delta in delta_values:
    v = t + delta
    w = t - delta
    finite_model = ssh(v, w).cut_piece(n_cells, 0)
    evals, evecs = finite_model.solve_ham(return_eigvecs=True)
    evals_d.append(evals)
    evecs_d.append(evecs)

The dispersion versus \(w\) reveals a pair of mid-gap levels once \(|w| > |v|\). Evaluating the wavefunction of one of those states confirms that it is exponentially localised at the boundary.

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))

# Plot the eigenvalues
ax1.plot(delta_values, evals_d, c="r")

ax1.set_title(f"Finite SSH Chain with {n_cells} unit cells")
ax1.set_xlabel(r"$\delta$")
ax1.set_ylabel(r"$E$")
ax1.set_ylim(-2, 2)

# Plot edge state density
band_idx = n_cells
delta_idx = 40
ax1.scatter(
    delta_values[delta_idx],
    evals_d[delta_idx][band_idx],
    color="b",
    s=70,
    marker="*",
    label="Edge state",
    zorder=2,
)
ax1.legend()
density = np.abs(evecs_d[delta_idx][band_idx, :]) ** 2
x_position = np.arange(len(density)) / 2
ax2.plot(x_position, density)
ax2.set_xlabel(r"$x$")
ax2.set_ylabel(r"$|\psi(x)|^2$")
ax2.set_title(rf"Edge state density at $\delta={delta_values[delta_idx]:.3f}$")
Text(0.5, 1.0, 'Edge state density at $\\delta=-0.200$')
../_images/4fdbaa699145e6b558a2b39535112f4a809f89d4d477fe6773a78445e4f7dcb7.png

Bulk polarization from Berry phases#

We compare the Berry-phase-derived polarizations for the trivial (\(|w| < |v|\)) and topological (\(|w| > |v|\)) regimes using a 1D \(k\)-mesh and WFArray.berry_phase. Values are reported modulo the lattice constant.

nk = 100
mesh = Mesh(["k"])
mesh.build_grid(shape=(nk,), gamma_centered=True)

v = 1.0
model_triv = ssh(v, 0.2)
model_top = ssh(v, 1.2)
lattice = model_triv.lattice

wfa_triv = WFArray(lattice, mesh)
wfa_triv.solve_model(model_triv)
P_triv = wfa_triv.berry_phase(0, [0]) / (2 * np.pi)

wfa_top = WFArray(model_top.lattice, mesh)
wfa_top.solve_model(model_top)
P_top = wfa_top.berry_phase(0, [0]) / (2 * np.pi)

print(f"Polarization |w|<|v|: {P_triv:.3f}")
print(f"Polarization |w|>|v|: {P_top:.3f}")
Polarization |w|<|v|: -0.000
Polarization |w|>|v|: 0.500

Next steps

  • Sweep both hoppings on a 2D grid to map the phase boundary where the mid-gap modes emerge.

  • Add an onsite staggered potential to break chiral symmetry and track how the edge states shift.

  • Couple two SSH chains with a weak inter-chain hopping and investigate how the edge spectrum hybridises.