Coding the Bose-Hubbard Hamiltonian with QuSpin

The purpose of this tutorial is to teach the interested user to construct bosonic Hamiltonians using QuSpin. To this end, below we focus on the Bose-Hubbard model (BHM) of a 1d chain. The Hamiltonian is $$ H = -J\sum_{j=0}^{L-1}(b^\dagger_{j+1}b_j + \mathrm{h.c.})-\mu\sum_{j=0}^{L-1} n_j + \frac{U}{2}\sum_{j=0}^{L-1}n_j(n_j-1)$$ where $J$ is the hopping matrix element, $\mu$ -- the chemical potential, and $U$ -- the interaction strength. We label the lattice sites by $j=0,\dots,L-1$, and use periodic boundary conditions.

First, we load the required packages:


In [ ]:
from quspin.operators import hamiltonian # Hamiltonians and operators
from quspin.basis import boson_basis_1d # Hilbert space boson basis
import numpy as np # generic math functions

Next, we define the model parameters:


In [2]:
##### define model parameters #####
L=6 # system size
J=1.0 # hopping
U=np.sqrt(2.0) # interaction
mu=2.71 # chemical potential

In order to construct the Hamiltonian of the BHM, we need to construct the bosonic basis. This is done with the help of the constructor boson_basis_1d. The first required argument is the chain length L. As an optional argument one can also specify the number of bosons in the chain Nb. We print the basis using the print() function.


In [3]:
##### construct Bose-Hubbard Hamiltonian #####
# define boson basis with 3 states per site L bosons in the lattice
basis = boson_basis_1d(L,Nb=L) # full boson basis
print(basis)


reference states: 
       0.  |6 0 0 0 0 0>
       1.  |5 1 0 0 0 0>
       2.  |5 0 1 0 0 0>
       3.  |5 0 0 1 0 0>
       4.  |5 0 0 0 1 0>
       5.  |5 0 0 0 0 1>
       6.  |4 2 0 0 0 0>
       7.  |4 1 1 0 0 0>
       8.  |4 1 0 1 0 0>
       9.  |4 1 0 0 1 0>
      10.  |4 1 0 0 0 1>
      11.  |4 0 2 0 0 0>
      12.  |4 0 1 1 0 0>
      13.  |4 0 1 0 1 0>
      14.  |4 0 1 0 0 1>
      15.  |4 0 0 2 0 0>
      16.  |4 0 0 1 1 0>
      17.  |4 0 0 1 0 1>
      18.  |4 0 0 0 2 0>
      19.  |4 0 0 0 1 1>
      20.  |4 0 0 0 0 2>
      21.  |3 3 0 0 0 0>
      22.  |3 2 1 0 0 0>
      23.  |3 2 0 1 0 0>
      24.  |3 2 0 0 1 0>
                 :
     437.  |0 0 0 4 2 0>
     438.  |0 0 0 4 1 1>
     439.  |0 0 0 4 0 2>
     440.  |0 0 0 3 3 0>
     441.  |0 0 0 3 2 1>
     442.  |0 0 0 3 1 2>
     443.  |0 0 0 3 0 3>
     444.  |0 0 0 2 4 0>
     445.  |0 0 0 2 3 1>
     446.  |0 0 0 2 2 2>
     447.  |0 0 0 2 1 3>
     448.  |0 0 0 2 0 4>
     449.  |0 0 0 1 5 0>
     450.  |0 0 0 1 4 1>
     451.  |0 0 0 1 3 2>
     452.  |0 0 0 1 2 3>
     453.  |0 0 0 1 1 4>
     454.  |0 0 0 1 0 5>
     455.  |0 0 0 0 6 0>
     456.  |0 0 0 0 5 1>
     457.  |0 0 0 0 4 2>
     458.  |0 0 0 0 3 3>
     459.  |0 0 0 0 2 4>
     460.  |0 0 0 0 1 5>
     461.  |0 0 0 0 0 6>

If needed, we can specify the on-site bosonic Hilbert space dimension, i.e. the number of states per site, using the flag sps=int. This can help study larger systems of they are dilute.


In [4]:
basis = boson_basis_1d(L,Nb=L,sps=3) # particle-conserving basis, 3 states per site
print(basis)


reference states: 
       0.  |2 2 2 0 0 0>
       1.  |2 2 1 1 0 0>
       2.  |2 2 1 0 1 0>
       3.  |2 2 1 0 0 1>
       4.  |2 2 0 2 0 0>
       5.  |2 2 0 1 1 0>
       6.  |2 2 0 1 0 1>
       7.  |2 2 0 0 2 0>
       8.  |2 2 0 0 1 1>
       9.  |2 2 0 0 0 2>
      10.  |2 1 2 1 0 0>
      11.  |2 1 2 0 1 0>
      12.  |2 1 2 0 0 1>
      13.  |2 1 1 2 0 0>
      14.  |2 1 1 1 1 0>
      15.  |2 1 1 1 0 1>
      16.  |2 1 1 0 2 0>
      17.  |2 1 1 0 1 1>
      18.  |2 1 1 0 0 2>
      19.  |2 1 0 2 1 0>
      20.  |2 1 0 2 0 1>
      21.  |2 1 0 1 2 0>
      22.  |2 1 0 1 1 1>
      23.  |2 1 0 1 0 2>
      24.  |2 1 0 0 2 1>
                 :
     116.  |0 1 2 2 0 1>
     117.  |0 1 2 1 2 0>
     118.  |0 1 2 1 1 1>
     119.  |0 1 2 1 0 2>
     120.  |0 1 2 0 2 1>
     121.  |0 1 2 0 1 2>
     122.  |0 1 1 2 2 0>
     123.  |0 1 1 2 1 1>
     124.  |0 1 1 2 0 2>
     125.  |0 1 1 1 2 1>
     126.  |0 1 1 1 1 2>
     127.  |0 1 1 0 2 2>
     128.  |0 1 0 2 2 1>
     129.  |0 1 0 2 1 2>
     130.  |0 1 0 1 2 2>
     131.  |0 0 2 2 2 0>
     132.  |0 0 2 2 1 1>
     133.  |0 0 2 2 0 2>
     134.  |0 0 2 1 2 1>
     135.  |0 0 2 1 1 2>
     136.  |0 0 2 0 2 2>
     137.  |0 0 1 2 2 1>
     138.  |0 0 1 2 1 2>
     139.  |0 0 1 1 2 2>
     140.  |0 0 0 2 2 2>

Often times, the model under consideration has underlying symmetries. For instance, translation invariance, parity (reflection symmetry), etc. QuSpin allows the user to construct Hamiltonians in symmetry-reduced subspaces. This is done using optional arguments (flags) passed to the basis constructor.

For instance, if we want to construct the basis in the $k=0$ many-body momentum sector, we do this using the flag kblock=int. This specifies the many-body momentum of the state via $k=2\pi/L\times\texttt{kblock}$.

Whenever symmetries are present, the print() function returns one representative from which one can obtain all 'missing' states by applying the corresponding symmetry operator. It is important to note that, physically, this representative state stands for the linear combination of vectors in the class, not the state that is displayed by print(basis).


In [5]:
basis = boson_basis_1d(L,Nb=L,sps=3,kblock=1) # ... and zero momentum sector
print(basis)


reference states: 
      0.  |2 2 2 0 0 0>
      1.  |2 2 1 1 0 0>
      2.  |2 2 1 0 1 0>
      3.  |2 2 1 0 0 1>
      4.  |2 2 0 2 0 0>
      5.  |2 2 0 1 1 0>
      6.  |2 2 0 1 0 1>
      7.  |2 2 0 0 2 0>
      8.  |2 2 0 0 1 1>
      9.  |2 1 2 1 0 0>
     10.  |2 1 2 0 1 0>
     11.  |2 1 2 0 0 1>
     12.  |2 1 1 2 0 0>
     13.  |2 1 1 1 1 0>
     14.  |2 1 1 1 0 1>
     15.  |2 1 1 0 2 0>
     16.  |2 1 1 0 1 1>
     17.  |2 1 0 2 0 1>
     18.  |2 1 0 1 2 0>
     19.  |2 1 0 1 1 1>
     20.  |2 0 2 0 1 1>
     21.  |2 0 1 1 1 1>
The states printed do NOT correspond to the physical states: see review arXiv:1101.3281 for more details about reference states for symmetry-reduced blocks.

Additionally, the BHM features reflection symmetry around the middle of the chain. This symmetry block-diagonalises the Hamiltonian into two blocks, corresponding to the negative and positive eigenvalue of the parity operator. The corresponding flag is pblock=+1,-1.


In [6]:
basis = boson_basis_1d(L,Nb=L,sps=3,kblock=0,pblock=1) # ... + zero momentum and positive parity
print(basis)


reference states: 
      0.  |2 2 2 0 0 0>
      1.  |2 2 1 1 0 0>
      2.  |2 2 1 0 1 0>
      3.  |2 2 1 0 0 1>
      4.  |2 2 0 2 0 0>
      5.  |2 2 0 1 1 0>
      6.  |2 1 2 1 0 0>
      7.  |2 1 2 0 1 0>
      8.  |2 1 1 2 0 0>
      9.  |2 1 1 1 1 0>
     10.  |2 1 1 1 0 1>
     11.  |2 1 1 0 2 0>
     12.  |2 1 1 0 1 1>
     13.  |2 1 0 2 1 0>
     14.  |2 1 0 2 0 1>
     15.  |2 1 0 1 2 0>
     16.  |2 0 2 0 2 0>
     17.  |1 1 1 1 1 1>
The states printed do NOT correspond to the physical states: see review arXiv:1101.3281 for more details about reference states for symmetry-reduced blocks.

Now that we have constructed the basis in the symmetry-reduced Hilbert space, we can construct the Hamiltonian. It will be hepful to cast it in the fllowing form:

$$H= -J\sum_{j=0}^{L-1}(b^\dagger_{j+1}b_j + \mathrm{h.c.})-\left(\mu+\frac{U}{2}\right)\sum_{j=0}^{L-1} n_j + \frac{U}{2}\sum_{j=0}^{L-1}n_jn_j $$

We start by defining the site-coupling lists. Suppose we would like to define the operator $\sum_j \mu_j n_j$. To this, end, we can focus on a single summand first, e.g. $2.71 n_{j=3}$. The information encoded in this operator can be summarised as follows:

  • the coupling strength is $\mu_{j=3}=2.71$ (site-coupling lists),
  • the operator acts on site $j=3$ (site-coupling lists),
  • the operator is the density $n$ (operator-string, static/dynamic lists)

In QuSpin, the first two points are grouped together, defininging a list [mu_j,j]=[2.71,3], while the type of operator we specify a bit later (see parantheses). We call this a site-couling list. Summing over multiple sites then results in a nested list of lists:


In [7]:
# define site-coupling lists
hop=[[-J,i,(i+1)%L] for i in range(L)] #PBC
interact=[[0.5*U,i,i] for i in range(L)] # U/2 \sum_j n_j n_j
pot=[[-mu-0.5*U,i] for i in range(L)] # -(\mu + U/2) \sum_j j_n

print(hop)
#print(interact)
#print(pot)


[[-1.0, 0, 1], [-1.0, 1, 2], [-1.0, 2, 3], [-1.0, 3, 4], [-1.0, 4, 5], [-1.0, 5, 0]]

The site coupling lists specify the sites on which the operators act, yet we need to tell QuSpin which operators are to act on these (pairs of) sites. Thus, we need the following operator strings which enter the static and dynamic lists used to define the Hamiltonian. Since the BHM is time-independent, we use an empty dynamic list


In [8]:
# define static and dynamic lists
static=[['+-',hop],['-+',hop],['n',pot],['nn',interact]]
dynamic=[]

print(static)


[['+-', [[-1.0, 0, 1], [-1.0, 1, 2], [-1.0, 2, 3], [-1.0, 3, 4], [-1.0, 4, 5], [-1.0, 5, 0]]], ['-+', [[-1.0, 0, 1], [-1.0, 1, 2], [-1.0, 2, 3], [-1.0, 3, 4], [-1.0, 4, 5], [-1.0, 5, 0]]], ['n', [[-0.7071067811865476, 0], [-0.7071067811865476, 1], [-0.7071067811865476, 2], [-0.7071067811865476, 3], [-0.7071067811865476, 4], [-0.7071067811865476, 5]]], ['nn', [[0.7071067811865476, 0, 0], [0.7071067811865476, 1, 1], [0.7071067811865476, 2, 2], [0.7071067811865476, 3, 3], [0.7071067811865476, 4, 4], [0.7071067811865476, 5, 5]]]]

Building the Hamiltonian with QuSpin is now a one-liner using the hamiltonian constructor


In [9]:
# build Hamiltonian
H=hamiltonian(static,dynamic,basis=basis,dtype=np.float64)

print(H.todense())


Hermiticity check passed!
Symmetry checks passed!
Particle conservation check passed!
[[ 4.24264069e+00 -2.00000000e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00]
 [-2.00000000e+00  2.82842712e+00 -1.00000000e+00  0.00000000e+00
  -1.41421356e+00  0.00000000e+00 -2.00000000e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
  -2.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00]
 [ 0.00000000e+00 -1.00000000e+00  2.82842712e+00 -1.41421356e+00
   0.00000000e+00 -1.41421356e+00  0.00000000e+00 -2.82842712e+00
   0.00000000e+00  0.00000000e+00 -1.41421356e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00]
 [ 0.00000000e+00  0.00000000e+00 -1.41421356e+00  2.82842712e+00
   0.00000000e+00  0.00000000e+00 -2.82842712e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00]
 [ 0.00000000e+00 -1.41421356e+00  0.00000000e+00  0.00000000e+00
   4.24264069e+00 -2.00000000e+00  0.00000000e+00  0.00000000e+00
  -2.00000000e+00  0.00000000e+00  0.00000000e+00 -1.41421356e+00
   0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00]
 [ 0.00000000e+00  0.00000000e+00 -1.41421356e+00  0.00000000e+00
  -2.00000000e+00  2.82842712e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00 -2.00000000e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00]
 [ 0.00000000e+00 -2.00000000e+00  0.00000000e+00 -2.82842712e+00
   0.00000000e+00  0.00000000e+00  2.82842712e+00 -1.41421356e+00
  -2.82842712e+00  0.00000000e+00 -1.41421356e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00]
 [ 0.00000000e+00  0.00000000e+00 -2.82842712e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00 -1.41421356e+00  2.82842712e+00
   0.00000000e+00 -2.00000000e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00]
 [ 0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
  -2.00000000e+00  0.00000000e+00 -2.82842712e+00  0.00000000e+00
   2.82842712e+00 -2.00000000e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00]
 [ 0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00 -2.00000000e+00  0.00000000e+00 -2.00000000e+00
  -2.00000000e+00  1.41421356e+00 -3.00000000e+00 -2.82842712e+00
   0.00000000e+00 -2.00000000e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00 -4.89897949e+00]
 [ 0.00000000e+00  0.00000000e+00 -1.41421356e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00 -1.41421356e+00  0.00000000e+00
   0.00000000e+00 -3.00000000e+00  1.41421356e+00  0.00000000e+00
  -4.24264069e+00  0.00000000e+00 -2.00000000e+00 -2.00000000e+00
   0.00000000e+00  0.00000000e+00]
 [ 0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
  -1.41421356e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00 -2.82842712e+00  0.00000000e+00  2.82842712e+00
  -2.00000000e+00  0.00000000e+00 -2.82842712e+00 -1.41421356e+00
  -3.46410162e+00  0.00000000e+00]
 [ 0.00000000e+00 -2.00000000e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00 -4.24264069e+00 -2.00000000e+00
   1.41421356e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00]
 [ 0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00 -2.00000000e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00  2.82842712e+00 -2.00000000e+00 -4.00000000e+00
   0.00000000e+00  0.00000000e+00]
 [ 0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00 -2.00000000e+00 -2.82842712e+00
   0.00000000e+00 -2.00000000e+00  2.82842712e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00]
 [ 0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00 -2.00000000e+00 -1.41421356e+00
   0.00000000e+00 -4.00000000e+00  0.00000000e+00  2.82842712e+00
   0.00000000e+00  0.00000000e+00]
 [ 0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00  0.00000000e+00 -3.46410162e+00
   0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
   4.24264069e+00  0.00000000e+00]
 [ 0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00 -4.89897949e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00  0.00000000e+00  0.00000000e+00  0.00000000e+00
   0.00000000e+00 -6.66133815e-16]]

when the Hamiltonian is constructed, we see three messages saying that it passes three type of symmetries. QuSpin does checks under the hood on the static and dynamic lists to determine if they satisfy the requested symmetries in the basis. They can be disabled by parsing the following flags to the hamiltonian constructor: check_pcon=False, check_symm=False and check_herm=False.

We can now diagonalise H, and e.g. calculate the entanglement entropy of the ground state.


In [10]:
# calculate eigensystem
E,V=H.eigh()
E_GS,V_GS=H.eigsh(k=2,which='SA',maxiter=1E10) # only GS
print("eigenenergies:", E)
#print("GS energy is %0.3f" %(E_GS[0]))
# calculate entanglement entropy per site of GS
subsystem=[i for i in range(L//2)] # sites contained in subsystem
Sent=basis.ent_entropy(V[:,0],sub_sys_A=subsystem,density=True)['Sent_A']
print("GS entanglement per site is %0.3f" %(Sent))


eigenenergies: [-7.99770126 -3.371967   -2.69891138 -0.92278606 -0.4865458   0.25457733
  1.01974122  1.55460719  2.72081364  3.00993842  4.55888621  4.92864042
  5.48809307  5.89976351  6.52449767  6.96579982  8.54322063 12.0925935 ]
GS entanglement per site is 0.409

In [11]:
psi_k=V[:,0]
psi_Fock=basis.get_vec(psi_k)

print(psi_k.shape, psi_Fock.shape)


(18,) (729, 1)

In [ ]: