Investigating the Hubbard model with variational algorithms

A project done through the QC Mentorship program

Written by Warren Alphonso, advised by Ntwali Bashige.

Once realized, quantum computers would be an intellectual and engineering triumph, success in turning Nature’s powerful and mysterious laws into computational power. If our current understanding of quantum mechanics holds up, it would mean the ability to efficiently simulate any physical process.

But it might take decades to build a quantum computer powerful enough to catch up with classical computers after their 50-year headstart.

A question I’ve been trying to answer since I started studying quantum computing concerns its near-term usefulness: what exactly can near-term quantum computers tell us about the world that our classical physical heuristics can’t? I don’t have a great answer yet, but this question is the guiding principle for the following post.

Most near-term useful work will probably use variational algorithms. This project explores the most well-known of the variational algorithms: the Variational Quantum Eigensolver (VQE). We’ll use the VQE to analyze a fundamental model in condensed matter physics, the Hubbard model.

You’ve noticed the length of this post by now. It is intentional; I’ve favored a long, winding, wandering, uncertain path as the search for uses of a quantum computer. Let’s get started! [1] Note to the prospective reader: I’ve done my best to only assume you’ve read the four essays at QuantumCountry. Another thing: I don’t know much quantum mechanics, so instead of pretending like things make sense when I don’t understand them, I’ll explicitly say “This is true by magic” as a reminder that we’re assuming something without understanding it.

Table of Contents

The Hubbard model

Exploring the Variational Quantum Eigensolver (VQE)

Uncovering magnetism from the Hubbard model

The Hubbard model

It is almost like Nature knows you are blatantly cheating, but she gives you a passing grade anyway.

— Ntwali Bashige

A quantum mechanical solid!

When do we need to use quantum computers to analyze solids? Quantum computers are based on quantum mechanics, so we can intuit that materials whose properties and behavior are so quantum mechanical that we can’t hope to approximate them with classical methods will be good candidates for demonstrating the usefulness of quantum computers.

But what solids fit this description? From Griffiths’ Introduction to Quantum Mechanics,

Problem 1.21: In general, quantum mechanics is relevant when the de Broglie wavelength of the particle in question is greater than the characteristic size of the system (d). In thermal equilibrium at (Kelvin) temperature T, the average kinetic energy of a particle is p22m=32kBT (where kB is Boltzmann’s constant 1.380×1023JK1), so the typical de Broglie wavelength (λ=h/p) is λ=h3mkBT where h is Planck’s constant 6.626×1034Js.

The purpose of this problem is to anticipate which systems will have to be treated quantum mechanically, and which can be safely described classically.

The lattice spacing in a typical solid is around d=0.3nm. Find the temperature below which the free1 electrons in a solid are quantum mechanical. Below what temperature are the nuclei in a solid quantum mechanical? Use sodium as a typical case. Moral: The free electrons in a solid are always quantum mechanical; the nuclei are almost never quantum mechanical.

1In a solid the inner electrons are attached to a particular nucleus, and for them the relevant distance would be the radius of the atom. But the outermost electrons are not attached, and for them the relevant distance is the lattice spacing. This problem pertains to outer electrons.

[2] We don’t know what or why the de Broglie wavelength is useful. We just assume that’s correct. Treat it like magic.

We want to solve λ>3×1010m. To do that, we need to choose some value for mass m to plug in, so that the inequality is only a function of temperature T. The problem recommends using sodium:

Sodium is a highly reactive alkali metal. Image source.

Solving λ>d: h3mkBT>3×1010h3×10103mkB>Th2102093mkB>T

Solving for electrons:

The mass of an electron is m=9.109×1031kg. Plugging in h, m, and kB, we get T<1.2935×105=129350K

For some perspective on this result, the surface of the sun is about 6000K. Almost everywhere on Earth satisfies the above temperature inequality; the electrons in sodium always require quantum mechanical treatment.

Solving for the nucleus:

Sodium has atomic number 11, which means it has 11 protons. The most common isotope of Sodium is 23Na, which has 12 neutrons. Because the mass of an electron is about a thousand times smaller than the mass of a proton or a neutron, we’ll ignore them when calculating mass. With only protons and neutrons, our total mass is 111.672×1027+121.674×1027kg=3.848×1026kg

Plugging in h, m, and kB, we get T<3.062K

This temperature is near-zero. The lowest natural temperature recorded on Earth is 184K, so the above inequality is almost never met. The nucleus can usually be treated with classical methods.

Our takeaway is that outer electrons must be analyzed quantum mechanically while nuceli can be well approximated classically. We’ll focus on solids whose outer electrons majorly determine their properties.

Why are electrons important?

It’s not clear why we should care about the outer electrons of atoms in a solid.

But since we want to do something useful with quantum computers that can’t be done with classical computers, this seems like a good thread to follow!

Generalizing to the Hubbard model

The problem we’re attacking is: how do the outer electrons in a solid affect the properties of the entire solid?

We’ll need some way to model this problem before we can start making progress. Let’s make some simplifying assumptions:

  1. Assume the atoms (nuclei) aren’t moving. We can model this as a lattice of fixed sites.
    • The mass of an electron is more than a thousand times smaller than the mass of a proton. Since the nucleus is made up of protons and neutrons, we’re justified in thinking only of movement relative to the nucleus, i.e. we assume the nucleus is stationary.
  2. Assume the atoms only have one electron orbital. The Pauli exclusion principle states that each orbital can have a maximum of 2 electrons: a spin-up electron and a spin-down electron.
    • This might seem like leap at first, because atoms can have many electron orbitals. But because we know that the outer electrons must be treated quantum mechanically, we may ignore any inner orbitals. While there can be multiple outer electron orbitals, we choose to only consider one outer orbital. I don’t have a great justification for this; it’ll just make calculations easier if we focus on the simplest case.
  3. Assume 2 electrons interact with each other only if they’re in the same orbital.
    • Interaction strength is determined by proximity: two electrons interact very strongly if they’re very close. We’ve decided to only consider the strongest interactions.
  4. Assume electrons can only hop to another atom if that atom is directly adjacent to their current atom.
    • It’s easiest and most common for electrons to hop to the nearest orbital, so we’re only considering these hopping terms.

These 4 assumptions define the Hubbard model of solids. Pictorially, we can represent the Hubbard model on a 2-dimensional lattice as:

Image source.

We’re making good progress, but we’ll need more than a purely visual understanding of our model.

Defining creation and annihilation operators

From our 4 assumptions above, there seem to be 2 main ideas we need to express about the behavior of free electrons: the kinetic energy of an electron moving from site to site, and the potential energy of 2 electrons interacting with each other if on the same site.

Here, I introduce a concept that isn’t motivated but extremely useful: the creation and annihilation operators. These arise from studying the quantum harmonic oscillator. [3] I recommend Lecture 8 and Lecture 9 of MIT OCW 8.04 Spring 2013.

Since we’re describing electrons, the creation and annihilation operators have fermionic anticommutation relations. In other words, for two fermions on sites j and k, with spins σ and π, we have:

{ajσ,akπ}=δjkδσπ{ajσ,akπ}=0{ajσ,akπ}=0

[4] By the way, these curly braces are called the anticommutator operator. It’s just a shorthand for the sum of products of two operators: {A,B}=AB+BA. }

We can derive some interesting properties of operators that obey the above relations, namely the Pauli exclusion principle.

For now, I’ll quickly summarize what you need to know about these operators.

We call a the creation operator and a the annihilation operator. Their names reflect that ajσ creates a fermion on site j with spin σ while ajσ destroys a fermion on site j with spin σ.

I said I don’t know why these relations hold, but we can try to think about how we might have derived them by ourselves. We want some relation that makes it clear that a2 and a2 are both nonsense, since we can’t have two fermions of the same spin in the same place. Since we’re working in a vector space, we can set these both equal to 0, which means that applying either of these operations destroys our whole vector space. Notice another way of writing a2=0 is 2a2={a,a}=0 which is the exact anticommutation relation we have. The same logic works for a. This explain two of the three relations above.

How could we have come up with the first relation?

Recall from the quantum harmonic oscillator that a|n=n|n1 and a|n=n+1|n+1. Let’s enforce the rule that n can only be 0 or 1. Notice that if n is constrained to those values, then n=n and |n1=|1n. With this we can write a|n=n|1na|n=(1n)|1n

Now what is (aa+aa)|n? It simplifies to (2n22n+1)|n. Plugging in n=0 and n=1 results in the same thing: 2n22n+1=1, which means we can write that {a,a} is always equal to 1, and we get our first commutation relation!

One last useful operator: the number operator n^=aa: n^|n=aa|n=a(n|1n)=n2|n=n|n As you can see, it’s behavior is to multiply a state by the number of fermions that occupy it. When it’s clear, I’ll use n instead of n^ to denote the number operator.

The Hubbard Hamiltonian

The Hamiltonian of a system is the operator that represents its energy, H=KE+PE, where KE is kinetic energy and PE is potential energy. {% annotate The Hamiltonian’s eigenstates are the possible resulting states when we measure a system, each eigenstate’s corresponding eigenvalue is that state’s energy, and the Hamiltonian tells us exactly how a system evolves with time according to the Schrödinger equation. Why this operator turns out to be so important is a much harder question. For now, it’s magic. Using the language of creation and annihilation operators, we can write the Hamiltonian as H=tj,kσ(ajσakσ+akσajσ)+Ujnjnj

The first term represents the kinetic energy. It’s also called the tunneling term. Notice the notation j,k. I use this to denote we’re summing over adjacent sites j and k. The second term represents the potential energy. It’s also called the interaction term. Notice it’s nonlinear - we only add U if there are 2 electrons on a site.

This notation should seem weird. We almost never add unitary matrices in quantum computing, because the resulting sum isn’t generally unitary. But Hamiltonians must be Hermitian, and it’s easy to see that the sum of Hermitian matrices is also Hermitian. Still, what does a sum of matrices physically mean? If we multiply some vector by this matrix, we’re acting on it with the kinetic and potential energy matrices and our result is a sum of the results. So maybe we can intuitively think of a sum of Hermitian matrices as two processes happening simultaneously to our state. [5] This explanation is in Scott Aaronson’s recenly-published lecture notes on Quantum Information Science.

Notice our interaction term is only relevant if there are 2 electrons on some sites. If have n total sites, then we can have a maximum of 2n electrons, 1 spin-up and 1 spin-down on each site according to the Pauli exclusion principle. If we have less than or equal to n electrons, we can order them so that we never have any interaction energy U by placing at most one electron on each site. This isn’t exactly a physical behavior because it means all those configurations have the same energy. To fix this, we’ll add a chemical potential energy μ which scales with the total number of electrons: [6] I don’t have a good explanation of why the chemical potential is important. It seems to be how “accepting” the system is of additional particles. It adds a linear potential term, so that we don’t have a potential only if we have 2 electrons.

H=tj,kσ(ajσakσ+akσajσ)+Ujnjnjμj(nj+nj)

The Jordan-Wigner transformation

We’ve got analytic and visual ways to think of our model now, but how can we represent it on a quantum computer? Notice that our Hamiltonian calculates energy based on where electrons are, so it’s keeping track of the location of electrons. If there are n possible sites in our lattice, then we have 2n possible locations an electron could be: each site can have an up electron and a down electron. We call each of the 2n locations a spin-orbital. Now that we’ve simplified our model to deciding whether or not an electron is in a spin-orbital, we can represent it with 2n qubits, one for each spin-orbital. If we order our 2n qubits by having the n up spin-orbitals be represented in the first n qubits, and then the n down spin-orbitals be represented in the last n qubits, if the j-th spin-orbital has an electron, then the j-th qubit is |1, and if the j-th spin-orbital doesn’t have an electron, then the n+j-th qubit is |0.

Now that we have this figured out, we need some way to encode our creation and annihilation operators into qubit operators. There are 3 main ways of doing this, but we’ll focus on the Jordan-Wigner transformation. [7] The 2 other popular ways to encode are the Bravyi-Kitaev and parity encodings. These are more complex than the Jordan-Wigner encoding but more useful in some situations.

The Jordan-Wigner transformation is very intuitive, and we can stumble across it ourselves. We need a to convert |0|1 and |10, while a must convert |1|0 and |00. Since qubits are 2-dimensional and we’ve specified the transforms of 2 basis vectors, we’ve completely defined the operators. Specifically, a=[0010]=XiY2a=[0100]=X+iY2

I’ve left out the subscripts, but remember that these operators are defined on the space of 2n qubits. So in reality, aj=I...XiY2...I and similarly for aj, which just means that we apply the identity to all other qubits and leave them alone.

But these don’t satisfy the anticommutation relations above! We require that ajak=akaj for any k and j. However, with our current encoding, we instead have ajak=akaj. (Try some examples to understand why this is the case.)

We need some way to to apply a 1 factor whenever we switch the order of the operators. Recall that the Pauli matrices satisfy AB=BA. If we prepend our operator with tensor products of the remaining unused Pauli matrix Z, then we can use this property to get the desired behavior. [8] This article by Microsoft is a great resource.

This is the Jordan-Wigner transform. We use the above formulation for a and a, then prepend the operator with Pauli Zs and append a sequence of identity operators I. In other words, a1=XiY2II...Ia2=ZXiY2I...Ia2n=ZZZ...XiY2

We can guess that these matrices become huge by looking at the chains of tensor products. For example, if we have a 4 site Hubbard model, we’d need 8 qubits for the JW transformation which means our Hilbert space has dimension 28=256. For a 10 site model our vectors have dimension 220=1048576. This gets out of hand quickly!

Mott gap

Most chemistry problems are concerned with finding the eigenstates and eigenvalues of the Hamiltonian. The most important eigenstate is the state corresponding to the lowest eigenvalue. We call this eigenstate the ground state and its corresponding eigenvalue the ground state energy. The name indicates that the eigenstates are configurations of the system that have a particular energy, specifically their corresponding eigenvalues. Why is the ground state so important? Because the system will tend toward its lowest energy state over time. Most systems spend the vast majority of their time in their ground state, so if we can understand its properties well, we can understand most of the behavior of the system.

Before we decide to use a quantum computer to figure out some properties of the Hubbard model, let’s try to make some progress with paper-and-pencil.

Some materials that were predicted to conduct electricity turned out to be insulators, especially at low temperatures. These are called Mott insulators, and their behavior is due to electron-electron interactions (like U in the Hubbard model) that classical theories don’t account for. We’ll use the Hubbard model to understand this!

Let’s do a simple version: the one-site case. Since there’s only one site, we have no hopping terms. Since electrons aren’t moving, it’s easy to see that we have 4 eigenstates: |0,|,|,|↑↓, where |0 indicates we have no electrons, and the arrows indicate the existence of an electron with that particular spin. By plugging in these eigenstates into the Hamiltonian, we can see that their eigenvalues are 0,μ,μ,U2μ, respectively.

With this, we can calculate the partition function of the system. [9] I don’t yet understand how the partition function is so powerful, but we’ll see below that it allows us to calculate some important quantities. It’s true by magic.

It’s defined as Z=Tr(eβH)=ii|eβH|i=i=0,,,↑↓eβEii|i=1+2eβμ+eβU+2βμ where β=1kBT.

The partition function lets us calculate expectations easily. For any observable m, m=Z1Tr(meβH). We will find the density ρ=n for number operator n. Using the previous formula, we have ρ=n=Z1Tr(neβEi)=Z1ieβEii|n|i=Z1(0+eβμ+eβμ+2eβ(U2μ))=2Z1(eβμ+e2βμβU)

I graphed the density ρ for temperatures T=0.5 and T=2 K. Try changing the potential energy U. Can you explain the result based on the Hubbard Hamiltonian?

−2−1012345678900.511.52
T = 0.5 KelvinT = 2 KelvinPotential energy U (Joules): 4.0-1.0-0.50.00.51.01.52.02.53.03.54.04.55.05.56.06.57.07.5Mott Insulating Gap on Single Site Hubbard ModelChemical potential μDensity ρ

Clearly there is some plateau where ρ becomes 1 at low temperatures when U becomes large. Upon reflection, this isn’t all that mysterious. Up until we have half-filling (1 electron on every site), U doesn’t affect the energy calculation at all because it’s only relevant if there are 2 electrons on a site. But once we hit this limit, the cost to add an additional electron is exactly Uμ. Unless μ is about equal to U, we’re stuck at half-filling. This is the exact behavior we see in the graph.

What’s harder to explain is why this doesn’t occur at high temperatures. Indeed, the effect is almost completely gone when the temperature increases by 1.5 Kelvin! I don’t have an answer to this yet.

Earlier, I motivated this by telling you about the Mott insulator. Let’s see if our graph gets us closer to a solution. We can think of a solid that conducts electricity as one in which electrons are able to move freely. Mott insulators, then, are materials in which electrons stop moving freely at low temperatures.

Our graph tells us that at low temperatures the Hubbard model is stuck at half-filling. Does this mean electrons can’t move? Yes, if every site has one electron then an electron hopping to a nearby site would incur a penalty of U. Electrons don’t move around, and the material stops conducting electricity.

But wait, this is just a guess! Our graph is only for a single site Hubbard model. Having to account for tunneling terms and more sites would make finding the eigenvalues/vectors (and thus calculating ρ) much harder.

This is the first problem we’ll try solving with a quantum computer. All we need to do is calculate ρ=n. In the Jordan-Wigner transformation, we encode each spin-orbital in a qubit, so all we have to do to find ρ is measure each qubit.

Exploring the Variational Quantum Eigensolver (VQE)

In this section, we’ll learn how to use VQE to learn properties of the 2×2 Hubbard model. People tend to focus on finding the ground state energy to high accuracy with VQE, but I honestly still don’t know why this is such an important quantity and why we need such high accuracy. We’ll find the ground state energy anyway because we get it for free when we do VQE, but I’m more interested in the ground state itself. Once we finish running VQE and find the ground state, we can analyze its properties.

In this section, we will discuss the measurement of interesting physical observables. The total energy… is the least interesting quantity. We will instead focus on densities and density correlations, kinetic energies, Green’s functions and pair correlation functions.

— Wecker et. al., Section 6 of 1506.05135

Representing the 2×2 Hubbard model

I’ll be using the OpenFermion software package for most of this post. OpenFermion is an open-source project that makes working with quantum chemistry much easier. [10] I originally planned to write my own code for representing and simulating the Hamiltonian for the Hubbard model, but it was wildly inefficient. My computer couldn’t handle anything more than a 2×2 Hubbard Hamiltonian, probably because I was storing the entire matrix. For the 2×2 case this translates to a 256×256 matrix. OpenFermion doesn’t use this approach, opting to store strings of creation and annihilation operators or scipy.sparse matrices. OpenFermion is built by Google, so they’ve made it easy to integrate with their own quantum computing library Cirq. We’ll be using Cirq as well later on.

A diagram of the 2 \times 2 Hubbard model with chemical potential \mu. Notice that this is not at half-filling.
Let’s create a 2×2 Hubbard lattice in OpenFermion:
from openfermion.utils import HubbardSquareLattice
# HubbardSquareLattice parameters
x_n = 2
y_n = 2
n_dofs = 1 # 1 degree of freedom for spin
periodic = 0 # Don't want tunneling terms to loop back around
spinless = 0 # Has spin
lattice = HubbardSquareLattice(x_n, y_n, n_dofs=n_dofs, periodic=periodic, spinless=spinless)
view raw lattice.py hosted with ❤ by GitHub
Next, we’ll want to create a FermiHubbardModel instance from our lattice. The format for the tunneling and interaction coefficients is a little weird — check out the documentation for an explanation.
from openfermion.hamiltonians import FermiHubbardModel
from openfermion.utils import SpinPairs
tunneling = [('neighbor', (0, 0), 1.)]
interaction = [('onsite', (0, 0), 2., SpinPairs.DIFF)]
potential = [(0, 1.)] # Must be U/2 for half-filling
mag_field = 0.
particle_hole_sym = False
hubbard = FermiHubbardModel(lattice , tunneling_parameters=tunneling, interaction_parameters=interaction,
potential_parameters=potential, magnetic_field=mag_field,
particle_hole_symmetry=particle_hole_sym)
view raw model.py hosted with ❤ by GitHub

And that’s it. We can access the actual Hamiltonian with hubbard.hamiltonian().

VQE Primer

I’ll give only a short hand-wavy explanation of the VQE algorithms; for a more in-depth understanding I recommend Michał Stęchły’s article.

The VQE algorithm uses a principle in quantum mechanics called the variational principle, which states that ψ|H|ψE0 where E0 is the ground state energy and |ψ is any state vector. A simple way to see that this inequality holds: we can write any state vector |ψ as a linear combination of eigenvectors |ψ=iai|Ei, so H acting on this linear combination multiplies each eigenvector by its eigenvalue Ei and then we take the inner product with the initial state |ψ which had norm 1 so we’re left with iaiEi of each eigenvector, which is clearly minimized if |ψ=|E0, the ground state.

The variational principle tells us that if we minimize ψ|H|ψ we’ll find the ground state. Machine learning and optimization techniques have been getting pretty good in the past few years; the insight of VQE is that we should use classical optimization techniques with a quantum computer. How? We create a parameterized circuit, which changes its gates based on a few parameters that a classical optimizer chooses.

Here’s a circuit with 1 parameters: the rotation angle for the Rz gate. Varying the parameter produces different final states.

The number of parameters increases very quickly. This paper found an ansatz that can reach every single state in the 2-qubit state space; the ansatz has 24 parameters. Our Hubbard model is represented on an 8-qubit state space. The strategy of an ansatz that can reach every single state would probably require thousands of parameters and be incredibly hard to optimize. That’s why choosing a good ansatz is very important.

Once we have a state, how do we find ψ|H|ψ? The only way we can extract information from a quantum circuit is through measurement so we have to rephrase this quantity as a measurement somehow.

Lemma 0: The Pauli tensors form a basis for all Hamiltonians.

Proof: The Pauli matrices are: I=[1001]X=[0110]Y=[0ii0]Z=[1001]

Any 2-dimensional Hermitian operator can be written as: H=[abbc] where a,c are real. We can define a linear combination of the Pauli matrices as: a0I+a1X+a2Y+a3Z=[a0+a3a1ia2a1+ia2a0a3] for real ai. Notice the upper-right and lower-left elements are complex conjugates of each other which is what a Hermitian matrix requires. The upper-left and lower-right values can be any real number, we just choose a0 to be the midpoint between the desired two real numbers and then a3 the distance from the midpoint to the desired number.

This means the Pauli matrices form a basis for 2-dimensional Hermitian matrices. Then we simply use the definition of the tensor product, which states that for any two vector spaces V1 and V2 with basis {ei} and {fj}, the tensor product V1V2 is a vector space with basis {eifj}.

All Hamiltonians are Hermitian and have dimension 2n where n is the number of qubits they act on, so all Hamiltonians can be written as a linear combination of Pauli tensors.

Thus, we can write ψ|H|ψ as a linear combination of Pauli tensors, like ψ|12XYZ+12...|ψ.

This is useful because we can compute these with measurements. I’ll do a 1-qubit example: suppose we wanted to get ψ|Z|ψ. We can write |ψ as a linear combinations of Z’s eigenvectors: |ψ=c0|0+c1|1. Now, ψ|Z|ψ=(c00|+c11|)(c0|0c1|1)=|c0|2|c1|2 Hey, |c0|2 is the probability we measure |0 and |c1|2 is the probability we measure |1, so we can find these values by doing a bunch of measurements and averaging our results. This can be extended to multiple-qubit states by measuring |00, etc., and can be extended to other Pauli operators by changing basis so any Pauli operator’s eigenvectors are |0 and |1.

As a reminder, the VQE algorithm requires us to specify 3 things: an ansatz, an initial state, and some initial parameters. We’ll choose them in the next few sections.

The Variational Hamiltonian Ansatz

Adiabatic evolution

A mysterious result in quantum mechanics is the adiabatic theorem. To quote Wikipedia: “A physical system remains in its instantaneous eigenstate if a given pertubation is acted on it slowly enough and if there is a gap between the eigenvalue and the rest of the Hamiltonian’s spectrum.”

Suppose we have two Hamiltonians HA and HB, and we know an eigenstate of HA to be |ψA. Then we can define a new Hamiltonian H(s)=(1s)HA+sHB. Notice that as s goes from 0 to 1 our time-dependent Hamiltonian goes from HA to HB.

The adiabatic theorem tell us that if we evolve |ψA by eitH(s) while slowly changing s, the final resulting state will be the corresponding eigenvector of HB. (It will be clearer later what I mean by “corresponding eigenvector.”) This is useful if one Hamiltonian is diagonal, or at least easy to diagonalize, because then we can easily find its ground state. Then we adiabatically evolve this ground state to get the ground state of another Hamiltonian that is perhaps harder to diagonalize.

I haven’t proven the adiabatic theorem because I don’t know how to yet. We’re just assuming it’s true for now. Regardless, let me try to make things more concrete with an example.

The Schrödinger equation is id|ψdt=H|ψ which means, for time-independent H, the solution is |ψ(t)=eiHt|ψ(0) where we evolve for time t.

Suppose we choose 5 discrete times: H(0),H(.25),H(.50),H(.75),H(1). At each time step, we’ll simulate for t=1. Then, if an eigenvector of HA is |ψA, an eigenvector of HB is: |ψB=eiH(1)eiH(0.75)eiH(0.50)eiH(0.25)eiH(0)|ψA

Of course, this only works if we choose sufficiently many discrete values for s and a short enough time t.

The adiabatic theorem tells us this will work only if we choose sufficiently many values of s and a short enough time t. I wish I could prove some bounds for these, but I can’t yet. 😞

Here’s an example with 2×2 Hamiltonians. We’ll choose HA=Z and HB=X.
import numpy as np
import scipy.linalg as sp
import matplotlib.pyplot as plt
H_A = np.array( [[1, 0], [0, -1]] )
H_B = np.array( [[0, 1], [1, 0]] )
H = lambda s: (1-s)*H_A + s*H_B
psi_A = np.array([0, 1]) # The ground state of H_A
# If n=5, then we do 5 steps: H(0), H(0.25), H(0.5), H(0.75), H(1)
n = 5
t = 1
res = psi_A
for i in range(n):
s = i / (n-1)
res = np.dot(sp.expm(-1j * H(s) * t), res)
view raw adiabatic.py hosted with ❤ by GitHub

With n = 5, we get res = [0.299+0.668j, -0.137-0.668j] and np.dot(H_B, res) gives us res = [-0.137-0.668j, 0.299+0.668j], which isn’t quite an eigenvector.

However, if we set n = 50, we get res = [ 0.713-0.043j, -0.698+0.043j], which is very close to the eigenvector of the Pauli X matrix with eigenvalue 1. We started with the ground state of HA and ended in the ground state of HB, just like the adiabatic theorem predicts!

Let’s try one last example. Instead of HB=X, this time we’ll use HB=[5004]. When we use the same parameters as before (n = 50, t = 1), we get res = [0, 0.92175127+0.38778164j]. This is very close to [0, 1] which is the eigenvector with eigenvalue 4. We didn’t get the ground state! What went wrong?

We can get some intuition by plotting the eigenvalues of H(s) as we increase s:

In the first graph, there is no crossing of the eigenvalues. We started with the lowest eigenstate of Z and ended in the lowest eigenstate of X. In the second graph, however, there is a crossing! Though we started in the lowest eigenstate of Z, we didn’t end up in the lowest eigenstate of the final matrix. Instead, it’s almost like we “followed the line” that shows up in the graph. If we wanted to get the ground state of the final matrix, we would have had to start in the +1 eigenstate of Z.

Keep this in mind! It means we have to choose which state we start in carefully. We cannot simply start in the ground state everytime.

An ansatz based on adiabatic evolution

Adiabatic evolution is an interesting concept and seems very powerful — it allows us to find ground states of arbitrary Hamiltonians by starting with an easy Hamiltonian. The catch is that the change in s might have to be exponentially small.

Instead of calculating this ourselves, we can just turn it into an ansatz and let an optimizer choose the best values!

I’m going to rewrite the adiabatic evolution equation: |ψB=seitH(s)|ψA=seit(1s)HAitsHB|ψA=keθkAHA+θkBHB|ψA where the final equality just means I’m making θk a parameter that I want the optimizer to choose. If HA and HB commute, then we also have [11] For matrices A and B, it is not generally true that eA+B=eAeB. |ψB=keθkAHAeθkBHB|ψA

Here’s our strategy for the Hubbard model: we’ll set HA to be the tunneling term and HB to be the full Hamiltonian. This is because the tunneling term is quadratic which means that it’s a linear combination of operators that are products of at most 2 creation and annihilation operators. We have efficient algorithms to diagonalize these and easily find the eigenvalues and eigenvectors. But the interaction term (and therefore the entire Hubbard Hamiltonian) is quartic. There’s no efficient way to find its eigenspectrum.

OpenFermion has a lot of good ansatze already defined that we can use. The two most relevant ones for the Hubbard model are SwapNetworkTrotterAnsatz and SwapNetworkTrotterHubbardAnsatz. The former is a generic adiabatic evolution ansatz which has about 20 parameters per iteration for the 2×2 Hubbard, while the latter is a less accurate that has 3 parameters per iteration.

After many hours of testing different configurations, I came up with a custom ansatz that scales as 9 parameters per iteration for the 2×2 Hubbard and has the same accuracy as SwapNetworkTrotterAnsatz. The code is messy because I had to make slight modifications to some methods in the SwapNetworkTrotterAnsatz class, but it’s available here.

Using my custom ansatz:
from CustomSwapNetworkTrotterAnsatz import *
steps = 2
ansatz = CustomSwapNetworkTrotterAnsatz(hubbard, iterations=steps)
view raw ansatz.py hosted with ❤ by GitHub

We’ve got our ansatz now. But we also need to choose an initial state and specify initial parameters. We’ll go over choosing an initial state in the next section.

Turns out the SwapNetworkTrotterAnsatz class (which my custom ansatz inherits from) has a good default strategy for initial parameters if we don’t specify one. Here’s the docstring that explains it:
CustomSwapNetworkTrotterAnsatz.default_initial_params?? # Typing "??" after an API call in Jupyter Notebook shows you the source code
def default_initial_params(self) -> numpy.ndarray:
"""Approximate evolution by H(t) = T + (t/A)V.
Sets the parameters so that the ansatz circuit consists of a sequence
of second-order Trotter steps approximating the dynamics of the
time-dependent Hamiltonian H(t) = T + (t/A)V, where T is the one-body
term and V is the two-body term of the Hamiltonian used to generate the
ansatz circuit, and t ranges from 0 to A, where A is equal to
`self.adibatic_evolution_time`. The number of Trotter steps
is equal to the number of iterations in the ansatz. This choice is
motivated by the idea of state preparation via adiabatic evolution.
The dynamics of H(t) are approximated as follows. First, the total
evolution time of A is split into segments of length A / r, where r
is the number of Trotter steps. Then, each Trotter step simulates H(t)
for a time length of A / r, where t is the midpoint of the
corresponding time segment. As an example, suppose A is 100 and the
ansatz has two iterations. Then the approximation is achieved with two
Trotter steps. The first Trotter step simulates H(25) for a time length
of 50, and the second Trotter step simulates H(75) for a time length
of 50.
"""
view raw doc.py hosted with ❤ by GitHub

Choosing an initial state

We’ve got an ansatz and initial parameters. The final requirement for VQE is an initial state. Since our ansatz is inspired by adiabatic evolution, our initial state needs to be the ground state of HA. Since we’ve set HA to be the tunneling term, we’re going to find its ground state.

This section has a lot of math. While working through “Elementary Introduction to the Hubbard Model,” I found the appearance of the following ideas remarkable, so I’m including them.

I highly recommend you do the math with me, but if you find that tedious, the last subsection goes over code that can just find the ground state for us. This is cheating, but since I told you quadratic Hamiltonians ground states are efficiently solvable it’s an okay place to cheat.

Position to momentum transformation

We can apply a Fourier transform to our creation operators to change from position to momentum basis:

akσ=1Nleiklalσ where k is a discrete momentum and l is a discrete position eigenstate. In 1D, kl=kl.

If you’re not familiar with the Fourier transorm, right now you don’t lose much by thinking of this as defining new operators as linear combinations of the old ones. Below (and in most treatments of Fourier analysis), we use the concept of n-th roots of unity, ω, which satisfy ωn=1.

These are the three 3-rd roots of unity. If you raise any of them to the third power, you’ll get 1.

Lemma 1: 1Nlei(knkm)l=δn,m where kn=2πn/N.

Proof: 1Nlei(knkm)l=1Nlei2πnmNl=1Nlωl which follows fromsetting ω=ei2πnmN=1N1ωN1ω bydefinition offinite geometric series=0 since wN=1

Of course, here we asssumed ω1, otherwise we can’t use the finite geometric series formula. In that case, 1Nl1l=1. Either way, 1Nlei(knkm)l=δn,m.

Lemma 2: 1Nneikn(lj)=δl,j.

This uses the same strategy as the previous proof, so I’ll skip this proof.

These two lemmas rely on our definition of momentum as kn=2πn/N. From now on, when summing over kn I’ll write summing over k, ie kn=k.

Lemma 3: alσ=1Nkeiklakσ

Proof: 1Nkeiklakσ=1Nkjeikleikjajσ which follows by converting akσ to position basis=1Njajσkeik(jl)=jalσδl,j by Lemma2=alσ

Using these relations, it’s easy to verify that the creation and annihilation operators in the momentum basis obey the fermionic anticommutation relations. I won’t do that here; most of the proof is mechanical.

Lemma 4: kσakσakσ=lσalσalσ for momentum k and position l.

Proof:

kσakσakσ=1Nklmσeik(lm)alσamσ bytransformingboth operators to position basis=1Nlmσalσamσkeik(lm)=lmσalσamσδl,m by Lemma 2=lσalσalσ

Take a moment to make sense of this result. The sum of number operators over position is equal to the sum of number operators over momentum. Every fermion has its own position and momentum, so these are just two ways of counting up the total number of fermions. We could have predicted this property without doing the math.

The 1D tunneling term

Theorem 1: For U=0, the 1D Hubbard Hamiltonian in momentum basis is H=kσ(ϵkμ)akσakσ where ϵk=2tcosk.

Proof: Our strategy will be to convert kσϵkakσakσ to position basis, and then use Lemma 4 to convert μkσakσakσ to position basis.

kσϵkakσakσ=2tNklmσcos(k)eik(lm)alσamσ by transfoming both operators to position basis=2tNlmσalσamσkcos(k)eik(lm)=2tNlmσalσamσk12(eik+eik)eik(lm) by using theidentity cos(x)=(eix+eix)/2=tNlmσalσamσkeik(lm+1)+eik(lm1)

Notice that by Lemma 2, keik(lm±1)=0 if lm±10, otherwise it’s N. Thus, we simplify to only the adjacent terms! kσϵkakσakσ=tl,mσalσamσ

By Lemma 4, we can tack on the chemical potential term too, so we have H=kσ(ϵkμ)akσakσ

This took me by complete surprise. The tunneling term in the position basis looks strange because we have this awkward behavior of summing over neighbors with l,m. It’s crazy that summing over all momentum number operators and multiplying by some cosine somehow equals this awkward summation in the position basis.

This means that by switching to the momentum basis we diagonalize our tunneling term. In this basis, its eigenvectors are just [1,0,0,...],[0,1,0,...] and its eigenvaluesu are cos(k)μ.

The 2D tunneling term

Extending the previous result to 2 dimensions isn’t that hard.

Theorem 2: For U=0, the 2D Hubbard Hamiltonian is H=kσ(ϵkμ)akσakσ where ϵk=2t(coskk+cosky).

Proof: The proof follows many of the same steps in the Theorem 1. Again, we’ll consider the μ coefficient separately.

kσϵkakσakσ=1Nlmσalσamσkϵkeik(lm)=2tNlmσalσamσkx,kyeik(lm)(12)(eikx+eikx+eiky+eiky)=tNlmσalσamσkx,kyeikx(lxmx±1)eiky(lymy)+eikx(lxmx)eiky(lymy±1)

The final step follows from extending the dot product for 2 dimensions. I wrote ±1 for the two exponentials; this means there are 4 exponentials: one with +1 in the x direction, one with 1 in the x direction, +1 in y, and 1 in y. Notice that these 4 terms correspond to a difference of 1 in a single dimension, and by Lemma 2, we get the hopping term over neighbors again.

By Lemma 4, we can attach the chemical potential term to get H=kσ(ϵkμ)akσakσ

Choosing the states with best overlap

We’ve shown that a change of basis exists that makes it easy to find the eigenvalues/vectors of the tunneling and chemical potential terms in the Hubbard Hamiltonian. Eearlier, we noticed that adiabatic evolution doesn’t always work if we input the ground state as our initial state. This means we’ll have to try many different eigenvectors until we find one that converges.

Here’s how we’ll approach this:

  1. Let HA be the tunneling and chemical potential terms of the Hubbard Hamiltonian (the quadratic terms). Let HB be the full Hubbard Hamiltonian.
  2. Create a perturbed Hamiltonian: H(s)=(1s)HA+sHB for small s.
  3. Find the ground state of HB by brute-force. (This is cheating.)
  4. Find the eigenvectors of H(s) by brute-force. (This is cheating.)
  5. Try adiabatically evolving one of the eigenvectors of H(s) into the ground state of HB. Keep trying until we find one that works.
  6. This will be our starting state - if we let s0 we can make this arbitrarily close to a linear combination of HB’s eigenvectors.

This seems very complicated! Why can’t be just try a bunch of HA’s eigenvectors until we find one that works? Why do we have to introduce this perturbed Hamiltonian? The answer is that our starting state might be a linear combination of HA’s eigenvectors. By perturbing it and then solving, we let the full Hubbard Hamiltonian influence the state we pick so we’re more likely to find the correct superposition.

Here’s code for steps 1-4:
# Step 1: H_A is `quad_sparse` and H_B is `hub_sparse`
quad = hubbard.tunneling_terms() + hubbard.potential_terms()
quad_sparse = get_sparse_operator(quad)
from openfermion.transforms import get_sparse_operator
hub_sparse = get_sparse_operator(hubbard.hamiltonian())
# Step 2: the perturbed Hamiltonian is `per_sparse`
s = 1e-2
per_sparse = (1-s)*quad_sparse + s*hub_sparse
# Step 3: Brute-force solve `hub_sparse`
from openfermion.utils import get_ground_state
genergy, gstate = get_ground_state(hub_sparse)
print("Ground state energy: ", genergy)
# Step 4: Brute-force solve `per_sparse`.
# w_per[i] is the eigenvector associated with eigenvector v_per[:, i]
w_per, v_per = scipy.sparse.linalg.eigsh(per_sparse, k=np.shape(per_sparse)[0]-2, which='SA')
view raw spectrum.py hosted with ❤ by GitHub

Ground state energy: -6.828

Now we’ll run adiabatic evolution on eigenvectors v_per[:, i] until we find one that evolves to have high overlap with gstate. Code for step 5:
from openfermion.utils import inner_product
def overlap(a, b):
"""Calculates the overlap between vectors a and b. This metric is also known as fidelity. """
inner = inner_product(a, b)
return (np.conjugate(inner) * inner).real
H = lambda s: (1-s)*quad_sparse + s*hub_sparse
best_per_state = None
n = 5
t = 1
for col in range(v_per.shape[1]):
state = v_per[:, col]
for i in range(n):
s = i / (n-1)
e = scipy.linalg.expm(-1j * H(s) * t)
state = e.dot(state)
fid = overlap(state, gstate)
if fid > .9:
best_per_state = v_per[:, col]
print(
"Overlap with perturbed eigenvector and true ground state is: ",
overlap(best_per_state, gstate))
break

Overlap with perturbed eigenvector and true ground state is: 0.942

So our starting state has 94.2% overlap with the desired ground state already. Let’s see how much better VQE can get us.

How good is this ansatz?

Now we’ve got everything we need to run VQE: an ansatz, a starting state, and some initial parameters. Before we run the algorithm, let’s step back to think about what we’re expecting to happen.

At first glance, this ansatz seems pretty terrible. A good ansatz is one that explores most of the state space fairly evenly. The VHA only has access to states which our starting state evolves into.

How can we think about this more concretely? The paper “Expressibility and entangling capability of parameterized quantum circuits for hybrid quantum-classical algorithms” goes much more in-depth into this topic, but I didn’t understand most of it. Instead, I’ll use the following:

  1. Define a vector |ψ=1M(i=1MRi|sθU(θ)|s) where Ri are uniformly random unitary matrices and U(θ) is our ansatz parameterized with θ. We’ll choose these parameters θ to be uniformly distributed as well. |s is our starting state.
    • The “perfect” ansatz [12] We don’t actually use this because it would have a lot of parameters and be incredibly hard to optimize. is one that has an even chance of reaching any state vector. We can simulate this with uniformly random unitary matrices, since unitary matrices are just rotations of a vector in state space. The diagrams below offer a more intuitive understanding. Then |ψ is just the difference between our ansatz and the “perfect” ansatz.
  2. We’ll classify the badness of an ansatz by the norm of |ψ. The smaller the badness value, the better our ansatz.
    • Since |ψ is the difference between our ansatz and the “perfect” ansatz, the smaller the magnitude of the difference, the better the ansatz.
1-qubit Bloch sphere representation of which states various ansatze can “reach” with random parameters. Notice how the uniformly random unitary is most evenly distributed. Image Source.
Here’s code to evaluate our badness metric:
M = 500 # Larger M means more accurate results
N = len(best_per_state)
def random_unitary():
"""
Generate uniformly random unitary from U(N).
Credit to Jarrod McClean @ https://jarrodmcclean.com/integrating-over-the-unitary-group/
"""
Z = np.random.randn(N, N) + 1.0j * np.random.randn(N, N)
[Q, R] = sp.linalg.qr(Z)
D = np.diag(np.diagonal(R) / np.abs(np.diagonal(R)))
return np.dot(Q, D)
# ANSATZ 1: Uniformly random unitary vectors
random_unitary_vecs = 0
for i in range(M):
ran_vec = random_unitary() @ best_per_state
random_unitary_vecs += ran_vec
random_unitary_vecs /= M
# ANSATZ 2: VHA
vha_vecs = 0
for i in range(M):
params = np.random.uniform(-2, 2, 15) # Pick 15 random parameters for the ansatz
circuit = cirq.resolve_parameters(ansatz.circuit, ansatz.param_resolver(params))
final_state = circuit.final_wavefunction(best_per_state)
vha_vecs += final_state
vha_vecs /= M
# ANSATZ 3: Identity
i_vecs = 0
for i in range(M):
i_vecs += best_per_state
i_vecs /= M
# ANSATZ 4: Another uniformly random unitary vectors
random_unitary_vecs_2 = 0
for i in range(M):
ran_vec = random_unitary() @ best_per_state
random_unitary_vecs_2 += ran_vec
random_unitary_vecs_2 /= M
print(np.linalg.norm(random_unitary_vecs - vha_vecs)) # 0.191
print(np.linalg.norm(random_unitary_vecs - i_vecs)) # 0.999
print(np.linalg.norm(random_unitary_vecs - random_unitary_vecs_2)) # 0.064
view raw badness.py hosted with ❤ by GitHub

To contextualize our metric, the identity ansatz got a badness score of almost 1 while a uniformly random unitary ansatz got a badness score of about 0.06. Our ansatz got a badness score of around 0.20. This is much closer to the perfect ansatz than to the identity, which is a little surprising because our ansatz is so restricted and has very few parameters.

To be clear, this is a pretty naive metric for how good an ansatz is. Expressibility of all states isn’t nearly as important as the ansatz’ ability to reach the desired state. Regardless, it is pretty interesting that our ansatz’ badness is closer to the expressive limit than to the completely unexpressive limit.

Finding the ground state

from openfermioncirq import HamiltonianObjective, VariationalStudy
from openfermioncirq.optimization import ScipyOptimizationAlgorithm, OptimizationParams
# Define our objective function as the expectation of the Hubbard Hamiltonian
obj = HamiltonianObjective(hubbard.hamiltonian())
# Define our VariationalStudy
study = VariationalStudy(
'Hubbard-VHA',
ansatz,
obj,
initial_state=best_per_state)
# Choose our optimization algorithm
algorithm = ScipyOptimizationAlgorithm(kwargs={'method': 'L-BFGS-B'}, uses_bounds=True)
optimization_params = OptimizationParams(algorithm=algorithm)
# Optimize
result = study.optimize(optimization_params)
print("Optimal ground state energy is {}".format(result.optimal_value))
view raw VHA.py hosted with ❤ by GitHub

Optimal ground state energy is -6.764

Recall the true ground state energy was genergy = -6.828, so we were off by about 0.064.

For now, let’s see how much overlap the ground state that VHA outputted has with the true ground state.

from cirq import resolve_parameters
optimal_params = study.trial_results[0].optimal_parameters
VHA_ground_state = resolve_parameters(
study.circuit, study.ansatz.param_resolver(optimal_params)).final_wavefunction(best_per_state)
print("VHA ground state and true ground state have an overlap of {}".format(
overlap(VHA_ground_state, gstate)))
view raw VHA_overlap.py hosted with ❤ by GitHub

VHA ground state and true ground state have an overlap of 0.992

99.2% overlap is great, especially since we only did 2 iterations!

Uncovering magnetism from the Hubbard model

I set out with the goal of using near-term quantum computing to actually understand something about the world that our classical heuristics couldn’t teach us, and all we’ve managed so far is a jargon-riddled journey to solving the ground state of a simple model. This section aims to cross more into the physical world. The ground state we discovered earlier is a real physical configuration of some solid; we’ll now probe the magnetic properties of that solid.

Spin and magnetism

Electron spin is deeply connected to magnetism. I don’t yet understand the physics behind this so we’ll treat it like magic.

When many electrons have the same spin, we get ferromagnetism, which means all the magnetic moments are aligned and we get a strong magnetic moment. If electron spins are balanced, they cancel each other out and we get antiferromagnetism, which means the material doesn’t have a strong magnetic moment.

Ferromagnetic order on the left (Image source); antiferromagnetic order on the right (Image source).

So to figure out the magnetic properties of solids, we have to look at the spins of its electrons. An important quantity we’ll analyze is the local moment: m2=(nn)2. The local moment is 0 if the site is empty or has 2 spins, and 1 otherwise. We’ll begin our analysis with the simplest case: the single site Hubbard model at half-filling.

Recall that for this case we have the partition function: Z=1+2eβμ+eβU+2βμ=2+2eβμ where the last step follows from μ=U2 as the condition for half-filling. We can write the expectation of the local moment as: m2=(nn)2=n+n2nnby linearity ofexpectation=Z1(Tr (neβH)+Tr (neβH)2Tr (nneβH))=2Z1eβμ=eβμ1+eβμPluggingin Z1

Since β=1T and μ=U2, it’s clear that as U, m21. The potential U seems to encourage local moments.

On the other hand, for finite U, as T, m212. The temperature seems to prefer random configurations and inhibits strong local moments.

What about the tunneling coefficient? Does it affect local moments? Turns out it behaves like temperature: it inhibits local moments. To show this, we’d have to use the 2 site model, but I’ll skip the math for now.

We’ll test out our predictions: we expect that as U, we get magnetism, and that as t we get no magnetism.

Analyzing the ground state

First, let’s see how many non-zero elements the state vector of the ground state has:

total_elems = 0
for elem in VHA_ground_state:
if np.abs(elem) > tol:
total_elems += 1
print("The {}-dimensionsal ground state vector has {} non-zero elements.".format(
VHA_ground_state.shape[0], total_elems))

The 256-dimensionsal ground state vector has 26 non-zero elements.

Great! It’s very sparse. If there was only 1 non-zero element, then we could decompose it into tensor products in the computational basis easily. Let’s see what the individual elements are:

# Make sure norm is 1
print("Norm is {}".format(np.linalg.norm(VHA_ground_state)))
# This finds every distinct element in VHA_ground_state (up to a tolerance)
# and counts its occurences
common_elems = {}
for elem in VHA_ground_state:
# Check if it's already in common_elems
array_common_elems = [complex(e) for e in list(common_elems.keys())]
match = np.isclose(array_common_elems, elem, rtol=0, atol=tol)
if match.any():
common_elems[str(array_common_elems[match.argmax()])] += 1
else:
common_elems[str(elem)] = 1
print("The distinct elements in the ground state vector along with their counts are: \n", common_elems)

Norm is 1.000000238418579

The distinct elements in the ground state vector along with their counts are:

{‘0’: 230, ‘(-0.075-0.050j)’: 2, ‘(0.0758+0.050j)’: 2, ‘(0.129+0.086j)’: 8, ‘(-0.129-0.086j)’: 8, ‘(0.183+0.121j)’: 4, ‘(-0.366-0.243j)’: 2}

Interesting, there’s some symmetry here. I guess I could decompose this into a sum of 16 pure states, but I don’t think we’ll get much insight into the system with that.

Let’s just get the average measurement for each of the qubits:

from cirq import measure_state_vector
hundred_thousand_measurements = np.array(
[measure_state_vector(VHA_ground_state, range(2 *x_n*y_n))[0] for _ in range(100000)])
np.mean(hundred_thousand_measurements, axis=0)

[0.49965, 0.50138, 0.49962, 0.49814, 0.50122, 0.50082, 0.49951, 0.49966]

Well that’s boring! Each qubit has an average value of 0.5. They’re uniform.

Let’s think about what this means. We used the Jordan-Wigner encoding which means the qubit corresponding to a spin-orbital is |1 if there’s an electron in that spin-orbital, and |0 if there isn’t. If each qubit has 0.5 chance of being |1, we have an average of 0.58=4 electrons. This is half-filling, which is what we expected.

We know the average was half-filling. Now let’s check how many measurements actually yielded half-filling:

for trial in hundred_thousand_measurements:
if np.count_nonzero(trial) != x_n*y_n:
print(np.count_nonzero(trial))

Nothing printed, so we didn’t get a single measurement where we didn’t have four |1’s. This means I was wrong. The ground state distribution isn’t random: it’s entangled in some way to give us at most 4 electrons in the 4 site model. (Of course, we set μ in order to get this half-filling behavior but it’s nice to verify that it works 100% of the time.)

Well, this is more interesting. I wonder if the spin-up and spin-down results are correlated in any way. Our qubits are correlated with spin in the following way: the 0th qubit represents a spin-up electron on site 0, the 1st qubit represents a spin-down qubit on site 0, the 2nd qubit represents a spin-up qubit on site 1, the 3rd qubit represents a spin-down qubit on site 1, etc. This means if there are always 2 qubits with even indices and 2 qubits with odd indices, there are always 2 spin-up and 2 spin-down electrons in every ground state.

We can also check how often they’re on the same site, though I suspect we’ll never find 2 electrons on the same site because of the interaction energy U.

n_states_unequal_spins = 0
n_states_correlated_spins = 0
for trial in hundred_thousand_measurements:
spin_up_qubits = trial[::2]
spin_down_qubits = trial[1::2]
if np.count_nonzero(spin_up_qubits) != np.count_nonzero(spin_down_qubits):
n_states_unequal_spins += 1
for i in range(x_n*y_n):
if spin_up_qubits[i] == 1 and spin_down_qubits[i] == 1:
n_states_correlated_spins += 1
break
print("Number of trials with unequal spin-up and spin-down electrons: ", n_states_unequal_spins)
print("Number of trials with at least 2 electrons on the same site: ", n_states_correlated_spins)

Number of trials with unequal spin-up and spin-down electrons: 0

Number of trials with at least 2 electrons on the same site: 42095

Huh, looks like my second guess was wrong. It turns out that in about 2/5 of the trials, the ground state had a fully occupied site, which I didn’t expect because of the additional interaction energy associated with it.

Since we always have the same number of spin-up and spin-down electrons, the ground state is antiferromagnetic.

Now let’s check our earlier hypotheses:

  1. I set t=1,U=2000,μ=1. Here’s the number of spin-up electrons over 100,000 ground states:
  2. Now I set t=1000,U=2,μ=1. Here’re the results:

As we expected, as U we get lots of local moments but as t we get no local moments.