Link Search Menu Expand Document

Dynamical structure factor of the Heisenberg S=1/2 chain

We compute the ground state energy of the \(S=1/2\) Heisenberg chain with periodic boundary conditions in each momentum sector \(k\). The Hamiltonian is given by

\[H = J\sum_{\langle i,j \rangle} \mathbf{S}_i \cdot \mathbf{S}_j\]

where \(\mathbf{S}_i = (S_i^x, S_i^y, S_i^z)\) are the spin \(S=1/2\) operators and \(\langle i,j \rangle\) denotes summation over nearest-meighbor sites \(i\) and \(j\). We set \(J=1\).

First, the Lanczos coefficients are computed using hydra and written to file

#include <hydra/all.h>

int main() {
  using namespace hydra;
  using namespace arma;

  int n_sites = 16;
  int n_up = n_sites / 2;

  // Create nearest-neighbor Heisenberg model
  BondList bonds;
  for (int s = 0; s < n_sites; ++s) {
    bonds << Bond("HB", "J", {s, (s + 1) % n_sites});
  }
  bonds["J"] = 1.0;

  // compute groundstate (known to be at k=0)
  Log("Computing ground state ...");
  auto block = Spinhalf(n_sites, n_up);
  auto gs = groundstate(bonds, block);
  Log("done.");

  for (int q = 0; q < n_sites; ++q) {

    Log("Dynamical Lanczos iterations for q={}", q);

    complex phase = exp(2i * pi * q / n_sites);
    BondList S_of_q;
    for (int s = 0; s < n_sites; ++s) {
      S_of_q << Bond("SZ", pow(phase, s) / n_sites, s);
    }

    // Compute S(q) |g.s.>
    auto v0 = zero_state(block);
    apply(S_of_q, gs, v0);
    double nrm = norm(v0);
    v0 /= nrm;

    // Perform 200 Lanczos iterations for dynamics starting from v0
    auto tmat = lanczos_eigenvalues_inplace(bonds, block, v0.vector(), 0, 0.,
                                            200, 1e-7);
    auto alphas = tmat.alphas();
    auto betas = tmat.betas();

    // Write alphas, betas, and norm to file for further processing
    alphas.save(
        fmt::format("outfiles/alphas.N.{}.nup.{}.q.{}.txt", n_sites, n_up, q),
        raw_ascii);
    betas.save(
        fmt::format("outfiles/alphas.N.{}.nup.{}.q.{}.txt", n_sites, n_up, q),
        raw_ascii);
    std::ofstream of;
    of.open(
        fmt::format("outfiles/norm.N.{}.nup.{}.q.{}.txt", n_sites, n_up, q));
    of << nrm;
    of.close();
  }

  return EXIT_SUCCESS;
}

The produced data is the read from file and can be visualized using the following python script.

#!/usr/bin/env python
import numpy as np
import matplotlib.pyplot as plt

n_sites = 16
n_up = n_sites // 2

eta = 0.05
n_omegas = 100
max_omega = 4.0
omegas = np.linspace(0, max_omega, n_omegas)

# compute broadened spectrum
spectra = np.zeros((n_sites, n_omegas))
for q in range(n_sites):
    alphas = np.loadtxt("outfiles/alphas.N.{}.nup.{}.q.{}.txt".format(n_sites, n_up, q))
    betas = np.loadtxt("outfiles/betas.N.{}.nup.{}.q.{}.txt".format(n_sites, n_up, q))
    norm = np.loadtxt("outfiles/norm.N.{}.nup.{}.q.{}.txt".format(n_sites, n_up, q))

    # compute poles and weights from tridiagonal matrix
    tmat = np.diag(alphas[:50]) + np.diag(betas[:49], k=1) + np.diag(betas[:49], k=-1)
    eigs, evecs = np.linalg.eigh(tmat)
    e0 = eigs[0]
    poles = eigs - e0
    weights = (norm**2) * (evecs[0, :]**2)
    print(norm, e0)
    
    # compute broadened spectrum with Gaussian broadening eta
    diffs = np.subtract.outer(omegas, poles)
    gaussians = np.exp(-(diffs / (2*eta))**2) / (eta * np.sqrt(2*np.pi))
    spectra[q, :] = gaussians @ weights

p = plt.imshow(spectra.T, origin='lower', extent=[0, n_sites, 0, max_omega],
           interpolation=None, aspect='auto')
plt.xlabel(r"$q$")
plt.ylabel(r"$\omega$")
plt.colorbar(p)
plt.show()

This will plot the following image: