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.
It is almost like Nature knows you are blatantly cheating, but she gives you a passing grade anyway.
— Ntwali Bashige
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 (
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
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
Solving
Solving for electrons:
The mass of an electron is
For some perspective on this result, the surface of the sun is about
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
Plugging in
This temperature is near-zero. The lowest
natural temperature recorded on Earth is
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.
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!
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:
These 4 assumptions define the Hubbard model of solids. Pictorially, we can represent the Hubbard model on a 2-dimensional lattice as:
We’re making good progress, but we’ll need more than a purely visual understanding of our model.
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
[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:
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
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
How could we have come up with the first relation?
Recall from the quantum harmonic oscillator that
Now what is
One last useful operator: the number operator
The Hamiltonian of a system is the operator that represents its
energy,
The first term represents the kinetic energy. It’s also called the
tunneling term. Notice the notation
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
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
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
I’ve left out the subscripts, but remember that these operators are
defined on the space of
But these don’t satisfy the anticommutation relations above! We
require that
We need some way to to apply a
This is the Jordan-Wigner transform. We use the above formulation for
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
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
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:
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
The partition function lets us calculate expectations easily. For any
observable
I graphed the density
Clearly there is some plateau where
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
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
This is the first problem we’ll try solving with a quantum computer.
All we need to do is calculate
In this section, we’ll learn how to use VQE to learn properties of
the
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
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 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.
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) |
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) |
And that’s it. We can access the actual Hamiltonian with
hubbard.hamiltonian()
.
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
The variational principle tells us that if we minimize
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
Lemma 0: The Pauli tensors form a basis for all Hamiltonians.
Proof: The Pauli matrices are:
Any 2-dimensional Hermitian operator can be written as:
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
All Hamiltonians are Hermitian and have dimension
Thus, we can write
This is useful because we can compute these with measurements. I’ll
do a 1-qubit example: suppose we wanted to get
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.
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
The adiabatic theorem tell us that if we evolve
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
Suppose we choose
Of course, this only works if we choose sufficiently many discrete
values for
The adiabatic theorem tells us this will work only if we choose
sufficiently many values of
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) |
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
Let’s try one last example. Instead of 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
We can get some intuition by plotting the eigenvalues of
In the first graph, there is no crossing of the eigenvalues.
We started with the lowest eigenstate of
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.
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
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:
Here’s our strategy for the Hubbard model: we’ll set
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
After many hours of testing different configurations, I came up with
a custom ansatz that scales as 9 parameters per iteration for the SwapNetworkTrotterAnsatz
. The code is messy
because I had to make slight modifications to some methods in the
SwapNetworkTrotterAnsatz
class, but it’s available here.
from CustomSwapNetworkTrotterAnsatz import * | |
steps = 2 | |
ansatz = CustomSwapNetworkTrotterAnsatz(hubbard, iterations=steps) |
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 theSwapNetworkTrotterAnsatz
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. | |
""" |
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
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.
We can apply a Fourier transform to our creation operators to change from position to momentum basis:
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
Lemma 1:
Proof:
Of course, here we asssumed
Lemma 2:
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
Lemma 3:
Proof:
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:
Proof:
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.
Theorem 1: For
Proof: Our strategy will be to convert
Notice that by Lemma 2,
By Lemma 4, we can tack on the chemical potential term too, so we
have
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
This means that by switching to the momentum basis we diagonalize our
tunneling term. In this basis, its eigenvectors are just
Extending the previous result to 2 dimensions isn’t that hard.
Theorem 2: For
Proof: The proof follows many of the same steps in the
Theorem 1. Again, we’ll consider the
The final step follows from extending the dot product for 2
dimensions. I wrote
By Lemma 4, we can attach the chemical potential term to get
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:
This seems very complicated! Why can’t be just try a bunch of
# 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') |
Ground state energy: -6.828
Now we’ll run adiabatic evolution on eigenvectorsv_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.
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:
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 |
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.
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)) |
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))) |
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!
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.
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:
Recall that for this case we have the partition function:
Since
On the other hand, for finite
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
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
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
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
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:
As we expected, as