diff --git a/config/_default/menus.en.toml b/config/_default/menus.en.toml
index 051ab2b..e4e4c84 100644
--- a/config/_default/menus.en.toml
+++ b/config/_default/menus.en.toml
@@ -38,4 +38,4 @@
# [[footer]]
# name = "Tags"
# pageRef = "tags"
-# weight = 10
+# weight = 20
diff --git a/content/_index.md b/content/_index.md
index 57a21f3..073b43f 100644
--- a/content/_index.md
+++ b/content/_index.md
@@ -15,16 +15,10 @@ Hi there! 👋
I am currently a Quantum Research Data Scientist at [Center for Computational Life Sciences, Lerner Research Institute, Cleveland Clinic](https://my.clevelandclinic.org/research/computational-life-sciences).
I am a physicist by training, with a Ph.D. from [Case Western Reserve University](https://case.edu/).
I work on interesting quantum computing problems in physics and life sciences.
-Beyond my [research](/research/), I am passionate about other emerging science and tech fields.
-Sometimes I write stuff on my [blog](/blog/).
+Beyond what I do in my [research](/research/), I am also broadly interested in emerging AI technologies.
+
-
-
-{{< alert "email">}}
-I am always open to collaborations and discussions. Please feel free to reach out to me via the links above.
-{{< /alert >}}
-
{{< button href="/about/" target="_self" >}}
diff --git a/content/research/index.md b/content/research/_index.md
similarity index 97%
rename from content/research/index.md
rename to content/research/_index.md
index e11bb90..ee92eb4 100644
--- a/content/research/index.md
+++ b/content/research/_index.md
@@ -12,7 +12,6 @@ showComments: false
-->
-
## Overview
My current research interests are focused on leveraging near-term quantum computers to solve problems in various domains, which include but are not limited to:
@@ -24,6 +23,11 @@ Things I have worked on in the past:
- **Topological materials**: investigating novel spin/charge transport phenomena in topological semimetals, such as Weyl and Dirac semimetals
- **Dark matter models**: studying the phenomenology of dark matter models in theories with a gauged baryon symmetry
+
+
+{{< alert "email">}}
+I am always open to collaborations and discussions. Please feel free to reach out to me via the links in my [homepage](../_index.md).
+{{< /alert >}}
{{ end }}
{{ end }}
diff --git a/public/404.html b/public/404.html
new file mode 100644
index 0000000..0786ff7
--- /dev/null
+++ b/public/404.html
@@ -0,0 +1,412 @@
+
+
+
+
Welcome! My name is Ruihao Li (李瑞浩).
+I grew up in Guangdong, China.
+I received my Bachelor’s degree with First Class Honours in physics from The University of Sydney (Australia) in 2016.
+After that, I began to pursue my Ph.D. in physics at Case Western Reserve University (CWRU) in 2017.
+I spent the first two years working with Dr. Pavel Fileviez Perez on theories of leptophobic dark matter models.
+From 2019, I started working with Dr. Shulei Zhang on charge and spin transport in topological quantum materials, especially topological semimetals.
+I graduated in 2023 and am currently working as a Quantum Research Data Scientist at Center for Computational Life Sciences, Lerner Research Institute, Cleveland Clinic, focusing on developing quantum computing applications in life sciences.
+I am also actively working on quantum computing projects in physics and am interested in developing open-source tools for research and education.
+I make use of both analytical and numerical tools for my research, including Mathematica, Python, Julia, etc.
+See my
+
+ research for more details.
Okay, I’ll admit that this is neither a super exciting nor a new topic to write about. But I’ve been looking into some ways to simulate/prepare thermal states these days and the Metropolis Monte Carlo algorithm (which is also often referred to as the Metropolis-Hastings algorithm) is probably one of the most well-known methods to do so. Before fully stepping into the quantum realm (see Refs. [1] and [2] for quantum generalizations of this method), I thought I might as well try my hand at the classical version first. I have decided to use this technique to simulate the ferromagnetic phase transition of the famous 2D Ising model in statistical physics. Instead of focusing on the physics or the algorithm itself, the main discussion of this post will revolve around how to use Numba and Cython to “supercharge” the Pythonic implementation.
Let us first say something about our objective here. Given a two-dimensional Ising model on an $L\times L$ square lattice, that is, $L^2$ spins which can only take values $+1$ (spin-up or red) or $-1$ (spin-down or blue) as displayed in the figure above, we want to simulate the thermal state of the system at finite temperature and extract a few thermal expectation values such as energy and magnetization along the way. Based on the evolution of net magnetization with an increasing temperature, we should observe a phase transition from a ferromagnetic (with finite magnetization) to a non-magnetic state (with zero magnetization) at some critical temperature $T_C$. Note that in 1D such phase transition will not happen due to the Peierls argument that dates back to the 1930s (see an explanation here). In its simplest form, the Ising Hamiltonian of the system is given by
+$$
+H = -J\sum_{\braket{i,j}}\sigma_i\sigma_j,
+$$
+where $\braket{i, j}$ denotes all pairs of neighboring spins on the lattice and $J$ is the coupling strength between any two spins.
+
To compute any thermal expectation, we need to know the probability distribution of different spin configurations, which, in this case, is the Boltzmann distribution since the system is essentially a canonical ensembleat equilibrium:
+$$
+P_S(\beta) = \frac{e^{-\beta E_S}}{Z},
+$$
+where $\beta = 1/k_B T$ is sometimes called the inverse temperature (for simplicity we will set the Boltzmann constant $k_B = 1$ hereafter) and $E_S$ is the energy of the spin configuration $S$, which can be computed from the Hamiltonian. $Z$ is the partition function defined as
+$$
+Z = \sum_S e^{-\beta E_S}.
+$$
+In particular, for $L^2$ spins, there are totally $2^{L^2}$ possible spin configurations. The partition function can be computed by summing over all these configurations, which is a computationally intractable task, especially for larger systems. However, we can use the Metropolis-Hastings algorithm to sample the distribution $P_S(\beta)$ and estimate the expectation values of interest. The basic idea is to start with a random spin configuration and then propose a new configuration by flipping a single spin. If the new configuration has a lower energy, we simply accept it. Otherwise, we accept it with probability $e^{-\beta(E_\text{new}-E_\text{old})}$, where $E_\text{new}$ and $E_\text{old}$ are the energies of the new and old configurations, respectively. This process is repeated for a large number of times until the sampled configurations are (almost) statistically independent and the distribution converges to the Boltzmann distribution. The expectation values are then computed from the sampled configurations by simply taking
+$$
+\braket{O(\beta)} = \sum_S O_S P_S(\beta)
+$$
+for any observable $O$.
+The Metropolis-Hastings algorithm is a Markov chain Monte Carlo (MCMC) method, which means that the new configuration is only dependent on the old configuration in the previous step and not on the entire history of the chain. This is a very important property of the algorithm, which allows us to parallelize the sampling process and speed up the computation. Here is a nice blog post by Gregory Gundersen justifying why this simple algorithm works if you want to have a deeper understanding of it.
+
Of course, the Metropolis algorithm is not perfect. You can imagine that in each step flipping just one spin could be inefficient. At low temperatures, spin flips are rare because the acceptance probability is low. So we would need to perform more steps to sample the distribution. More severely, as the system approaches the critical temperature, the spin-spin correlation length diverges, so the spins tend to form large aligned domains. This leads to a phenomenon called the critical slowing down, which reflects the difficulty in flipping a spin within a correlated spin cluster. One way to overcome this hurdle is to use a cluster-flipping algorithm which flips multiple spins at once. An example of this is the Wolff algorithm [3] but in this post we will just focus on the Metropolis algorithm.
Without further ado, let us implement the Metropolis algorithm for the 2D Ising model in Python. To start with, we can randomly generate the initial spin configuration. We can build a class called Ising with core methods to generate an initial spin configuration, compute the system’s total energy and net magnetization, and of course, update the spin configuration based on the Metropolis algorithm. Since the implementation is pretty straightforward, I will just attach the code below:
+
importnumpyasnp
+
+classIsing:
+"""
+ Pure Python implementation of Metropolis Monte Carlo Simulation of the 2D
+ Ising model (square lattice).
+ """
+
+def__init__(self,J,L,T):
+"""
+ Initialize the Ising model.
+
+ Args:
+ J (float): Coupling constant
+ L (int): Linear size of the lattice
+ T (float): Temperature
+ """
+self.J=J
+self.L=L
+self.T=T
+self.spin_config=self.generate_spin_config()
+
+defgenerate_spin_config(self):
+"""
+ Generate a random spin configuration.
+ """
+returnnp.random.choice([-1,1],size=(self.L,self.L))
+
+defcompute_energy(self):
+"""
+ Compute the energy of the spin configuration.
+ """
+energy=0
+foriinrange(self.L):
+forjinrange(self.L):
+# Periodic boundary conditions
+energy+=(
+-self.J*self.spin_config[i,j]*(
+self.spin_config[i,(j+1)%self.L]
++self.spin_config[(i+1)%self.L,j]
++self.spin_config[i,(j-1)%self.L]
++self.spin_config[(i-1)%self.L,j]
+)
+)
+returnenergy/2
+
+defcompute_magnetization(self):
+"""
+ Compute the net absolute magnetization of the spin configuration.
+ """
+returnnp.abs(np.sum(self.spin_config))
+
+defmc_update(self):
+"""
+ Perform L^2 Metropolis steps to update the spin configuration.
+ """
+for_inrange(self.L**2):
+# Pick a random site
+i=np.random.randint(self.L)
+j=np.random.randint(self.L)
+# Compute the change in energy
+delta_E=(
+2*self.J*self.spin_config[i,j]*(
+self.spin_config[i,(j+1)%self.L]
++self.spin_config[(i+1)%self.L,j]
++self.spin_config[i,(j-1)%self.L]
++self.spin_config[(i-1)%self.L,j]
+)
+)
+# Flip the spin if the energy decreases or if the Metropolis
+# criterion is satisfied
+ifdelta_E<=0:
+self.spin_config[i,j]*=-1
+elifnp.random.random()<np.exp(-delta_E/self.T):
+self.spin_config[i,j]*=-1
+
As one can see from mc_update(), one full update corresponds to repeating the Metropolis step $L^2$ times. The next configuration to be used in the Monte Carlo method is obtained after one full update. To set up the simulation, we will need to specify the values of the coupling strength $J$, the length of the square lattice $L$, as well as the temperature range within which to compute the expectation values $(T_\text{low}, T_\text{high})$. Furthermore, as mentioned before, we first need to perform enough Metropolis updates (specified by equil_steps) to sample the equilibrium distribution, after which we again update the system mc_steps times in total to build up the statistics for measuring the expectation values. We also define skip_steps which sets the number of lattice updates between any two consecutive measurements. Therefore, we can define the following function that outputs the energy expectation, magnetization expectation, and the final spin configuration at a range of temperatures:
+
defrun_ising(J,L,T_low,T_high,nT,equil_steps,mc_steps,skip_steps):
+"""
+ Run the Monte Carlo simulation for a range of temperatures.
+
+ Args:
+ J (float): Coupling constant
+ L (int): Linear size of the lattice
+ T_low (float): Lower bound of the temperature range
+ T_high (float): Upper bound of the temperature range
+ nT (int): Number of temperatures to simulate
+ equil_steps (int): Number of Monte Carlo steps to perform for equilibration
+ mc_steps (int): Number of Monte Carlo steps to perform during measurement
+ skip_steps (int): Number of Monte Carlo steps to skip between measurements
+
+ Returns:
+ (np.array, np.array, np.array): Arrays of energies (per site),
+ magnetizations (per site), and spin configurations
+ """
+T_array=np.linspace(T_low,T_high,nT)
+# Initialize arrays to store the energies and magnetizations
+E_array=np.zeros(nT)
+M_array=np.zeros(nT)
+# Initialize arrays to store the final spin configurations for each T
+spin_config_array=np.zeros((nT,L,L))
+# Loop over temperatures
+foriinrange(nT):
+Et=Mt=0
+# Initialize the Ising model
+ising=Ising(J,L,T_array[i])
+# Equilibrate the system
+for_inrange(equil_steps):
+ising.mc_update()
+# Perform Monte Carlo steps after equilibration
+forjinrange(mc_steps):
+ising.mc_update()
+# Skip the first few steps
+ifj%skip_steps==0:
+Et+=ising.compute_energy()
+Mt+=ising.compute_magnetization()
+# Average the energy and magnetization
+E_array[i]=Et/(mc_steps//skip_steps)/L**2
+M_array[i]=Mt/(mc_steps//skip_steps)/L**2
+# Store the final spin configuration
+spin_config_array[i]=ising.spin_config
+returnE_array,M_array,spin_config_array
+
Luckily for us, the 2D Ising model has closed-form solution for the energy and magnetization expectations, as well as the critical temperature $T_c$, which can be used to gauge the accuracy of our simulation. We can also have these exact solutions built in to our Ising class. The full code can be found here.
If you run the above code, you would find it quite slow even with lattice size as small as $L = 10$, which took around 50 seconds on my MacBook Pro. The main bottleneck of the code is the mc_update() function, which involves $L^2$ evaluations of delta_E in the Metropolis step and is called repeatedly in the Monte Carlo simulation. The mc_update() function above is written in pure Python, which is not very efficient. One easy way to accelerate the code is to use the Numba library. Numba is a just-in-time (JIT) compiler that translates Python code into machine code during runtime (rather than pre-runtime like some compiled languages such as C). As we know, Python is an interpreted language, which means that the source code is converted to bytecode and executed line by line. A (good) JIT compiler helps remove the interpreter overhead and also allows for better optimizations since it has access to dynamic runtime information. What’s cool about Numba is that it works at function level and you can simply annotate functions you want to accelerate with the @jit decorators. It works best when a function involves heavy NumPy operations and nested loops. In our case, we should obviously accelerate the mc_update() function, but also compute_energy() and compute_magnetization(). One caveat in this case is that we cannot directly “jit” instance methods of a class. There are multiple ways to get around it. One is to convert the instance methods to static methods. Another is to use @jitclass to decorate the whole class but it is hard to work with more complex classes. Finally, a way that I find most convenient is to define the functions to be “jitted” outside the class and then just call them in the corresponding methods. So we would have something like this:
Note that we have specified nopython=True in the @jit decorator (which is equivalent to using the @njit decorator) to ensure that the function is compiled in “nopython” mode, which means that the decorated function will run without the Python interpreter. The nopython mode gives the best performance but it also requires that the native types of all variables in the function can be inferred. Quoting from Numba’s official documentation: “A failure in type inference can be caused by two reasons. The first reason is user error due to incorrect use of a type. This type of error will also trigger an exception in regular python execution. The second reason is due to the use of an unsupported feature, but the code is otherwise valid in regular python execution.” If nopython=True is not set, the compilation would fall back to the object mode if type inference fails.
+
Overall, Numba is great when it works, leading to huge performance boost (as we will see later), and it has other features which I did not touch upon, such as parallelization and vectorization. However, in its current form, it is still quite limited in its nopython mode as only certain features in Python and NumPy are supported (see a full list here and here). Even when it is possible to make existing Python codes Numba-compatible, one often has to spend time refactoring them in certain ways to make it work.
Another popular way of supercharging Python codes is to use Cython. Cython is a superset of Python that allows you to write (slightly modified) Python code with static type declarations and compile it into C code (hence the name). The C code can then be compiled into a Python extension module that can be imported and used in Python. The main advantage of Cython is that it allows you to write codes that resemble Python but at the same time you can use all the features and functions of C (e.g., pointers, structs, etc.) and also take advantage of the C compiler’s optimizations. One downside is that it is not as straightforward as Numba since you need to write the code in a different syntax and compilation is always needed before using it. The basic usage of Cython is well documented here. Basically, two steps are needed: (1) write the Cython source code in a .pyx file and (2) compile the .pyx file into a Python extension module. The compilation can be done in two ways: (1) using the cython command line tool or (2) using the cythonize() function in the setuptools package. The second method is more convenient when using with an associated setup.py with the following basic structure:
Note that the Extension module is used in order to specify compiler options and use extra libraries. You can find the setup.py file used for this project here (inspired by Rajesh Singh’s work), where an additional check for OpenMP support is added. Cython comes with OpenMP support for parallel computing and to enable it, you need to supply '-fopenmp' as the argument in extra_compile_args and extra_link_args above. Within the Cython source code, you can use cython.parallel.prange() in place of range() for parallelizing loops. One thing to keep in mind is that the Global Interpreter Lock (GIL) must be released to use this kind of parallelism. One can release the GIL around a block of code using the with nogil: statement. Alternatively, one can specify nogil in a C function header to declare that it is safe to call without the GIL (but this does not guarantee that the function will be run with the GIL released):
+
cdefvoidmy_nogil_func(...)nogil:
+...
+
For our case, one good place to use parallelism is the outer loop of some nested loops. You can see more about parallelism in Cython here.
+
In addition to parallelism, there are other things one can do to make Cython faster. Two of the most frequently used function decorators to speed things up are @cython.boundscheck(False) (assuming no IndexErrors will be raised) and @cython.wraparound(False) (no negative indexing is used). However, you should make sure you know what they do and check your code carefully before using any of these. One pitfall I encountered as a newbie to C was the use of @cython.cdivision(True). I found that it did help speed things up quite a bit, but upon checking the computed energy and thermal expectations against the ones based on pure Python and Python with Numba, it led to some clearly incorrect results. It turns out that this is due to a different behavior of the operator % in Python and C. Unlike in Python where it represents a modulo operation, in C it is actually the reminder operation. For example, -10 % 3 = 2 in Python but doing the same in C leads to -1. To stick to the Pythonic behavior, I defined a separate function to perform the modulo operation in the cdivision mode:
The other thing about cdivision is that it does not do floating point division automatically, i.e., int / int = int. So again, one should watch out for unexpected behaviors like this when using Cython. Two blog posts I came across that provide many useful tips for Cython usage are here and here. For a list of complier directives one can toggle please refer to this document.
+
Finally you can find the full source code for my Cython implementation of the 2D Ising model simulation here.
So how did the Metropolis algorithm do? I ran the simulation with equil_steps = 5000, mc_steps = 10000, skip_steps = 100 for four different system sizes, $L = [10, 20, 40, 80]$. The following plots show the thermal expectations of energy and magnetization as functions of the temperature (from 0.5 to 4.5 with 50 points in total). The red lines correspond to the exact results and the dashed line denotes the exact critical temperature. In the energy plot, the Monte Carlo results match the exact result pretty well, but the magnetization plot shows some more noticeable discrepancies. In both cases, we can see that generally as the system size increases, the Monte Carlo results match the exact results better due to the fact that the exact solutions are obtained in the limit $L\to\infty$. At the same time, at low temperatures, there are more points that deviate significantly from the exact results as the system size grows. This is due to the fact that the Metropolis algorithm is not very efficient at low temperatures since the acceptance rate is very low.
+
+
We can also look at how the equilibrium spin configuration evolves with an increasing temperature. Here is an animation showing just that (for $L=80$). The behavior is consistent with the phase diagram of the 2D Ising model (also reflected in the magnetization plot above). At low temperatures, the spins tend to align in the same direction, while at high temperatures, they are randomly oriented, leading to an (almost) vanishing net magnetization.
+
What about the speed? The following plot shows the time it takes (normalized to the time taken by pure Python) to run the full simulation (with the same parameters introduced above) for several different system sizes.
+
+
Of course, this is by no means a rigorous benchmark for performance. But it does give us a rough idea of how much faster we can make the Metropolis Monte Carlo using Numba or Cython (50 - 250 times faster)! This is pretty important especially for larger systems when the absolute time used can be cut down significantly.
I am adding a comments widget to my blog. Now you can comment under the blog posts whenever as long as you have a GitHub account. I’ve always wanted one for the sole purpose of encouraging interactions and discussions. Just a place where you (readers) can quickly jot down your thoughts/comments/questions. Hugo, which is the the framework this website is built on, ships with support for Disqus. I guess Disqus is fine for general uses, but I wanted something more lightweight, free of ads, and has Markdown support. Luckily I came across Utterances today. It’s an open-source comments widget that is built on GitHub Issues, developed by Jeremy Danyow (Thank you!). I like its simplistic aesthetics and even better, there are multiple (dark) themes you can choose from. Most importantly, it supports Markdown in the comments. I think it would be even better if there is LaTeX support in the future! The general setup is quite simple and I think the official guide is all you really need.
+
While setting it up on my website (based on the Congo theme for Hugo) by directly following the instructions, I encountered two small problems. One is the positioning of the comments widget, which is centrally aligned by default but was just sitting a bit too far to the right in my current layout. My way of mitigating this is to add a <div> tag with a class attribute which I call comment to the comments.html file (placed under layouts/partials/), which contains the <script> tag for Utterances. Then inside custom.css (under assets/css/) I added the following:
This places the comments widget in a position to my liking.
+
The other problem is related to where the comment widget shows up. By default, having the comments.html file in the layouts/partials/ directory will make the widget appear in any page with an index.md associated with it. This is not the behavior I want because ideally the comments section should be restricted to blog posts only. There is a simple way to add a knob for this in Congo. First, inside config/_default/params.toml, I added showComment = true under the [article] section. This allows us to have a theme parameter article.showComment that can be set to false if we want to hide the comments section in a particular page. Then to link this knob to the Utterances widget, I inserted {{ if .Params.showComment | default (.Site.Params.article.showComment | default true) }} at the beginning of comments.html.
I just updated Congo to v2.3.0 today (July 6, 2022) and it now comes with the showComments support. See the implementation in layouts/_default/single.html, which automatically solves both of my problems mentioned above. So if you are using Congo v2.3.0 and above, ignore everything I said above. To display the Utterances widget in your blog posts, simply provide the script in layouts/partials/comments.html and set the showComments parameter to true in your config/_default/params.toml file.
This weekend I spent some time tweaking the Terminal on my Mac with iTerm2 and Fish shell. Why did I do it? First of all, I have been quite fed up with the boring black-and-white look of the default Terminal app. In my opinion, the introduction of multiple colors together with glyphs/icons not only makes it look better visually, it also helps one distinguish different contents (e.g. directories, files) more easily. Moreover, I just want more customizations and features like autosuggestions to make working with command lines a little more efficient. So below are the ingredients I used to customize my Terminal and instructions for some key steps.
Unzip and double click on the the color scheme Solarized Dark.itermcolors under the directory /iterm2-colors-solarized
+
Open iTerm2’s Preferences → Profiles → Colors, and select the theme under Color Presets
+
+
+
Text: We need one of the Nerd Fonts to render the glyphs/icons; I chose two of them
+
+
Meslo Nerd Font: Downloaded from the Tide repo; this particular font contains all the glyphs needed
+
Source Code Pro Nerd Font: Installed on Homebrew via brew tap homebrew/cask-fonts (only need to run this once) and then brew install --cask font-source-code-pro; This is the font I’d like to use in text
+
Alternatively, all the Powerline Fonts can be installed based on this repo
+
Go to iTerm2’s Preferences → Profiles → Text, under Font, choose Source Code Pro (for Powerline) or any font you like; select Use a different font for non-ASCII text, then choose MesloLGS NF under Non-ASCII Font
+
+
+
I also resize the New Windows to be 120 columns and 40 rows under the Window panel
While setting up fish on a M1 Mac yesterday, I realized that the fish shell path is not the same as in Intel-based Macs. So if you have a Mac with Apple Silicon, after brew install fish you can run:
+
+
fish_add_path /opt/homebrew/bin
+
echo "/opt/homebrew/bin/fish" | sudo tee -a /etc/shells
A few weeks ago I participated in the IBM Quantum Spring Challenge 2022, which was a fun challenge to do because one of the topics covered is actually close to my heart, which is on quantum simulations of many-body systems in condensed matter physics. In these problems, we investigated a well-known phenomenon (to the condensed matter physics community, of course) called Anderson localization and one called many-body localization, which I happened to gain some exposure to during the MAGLAB Theory Winter School earlier this year and is still an active topic of research. The majority of this Challenge was about introducing and reproducing some of the results from this nicely written quantum transport paper by Karamlou et al. I will split the full discussion into three parts. This blog post will cover the first part, where we will set up the framework for investigating many-body physics on a quantum computer. This includes building the tight-binding model for a 1-D quantum chain and using Trotterization for simulating dynamics of the quantum states.
+
The other topic of the Quantum Challenge was quantum chemistry calculations with the variational quantum eigensolver (VQE), which I do not intend to discuss this time. Interested readers are encouraged to take a look at the original announcement linked above for more details. Here is the official Github repository if you want to have a go at the challenge problems. Without further ado, let us begin our discussion.
The tight-binding model would be the building block for studying the many-body physics that will be discussed throughout this post. In layman’s terms, the tight-binding model describes a solid-state system where most electrons are “tightly bound” to their nuclei, which sit at the fixed lattice sites. Only a few valence electrons are loosely bound and therefore can “hop” to the neighboring sites. This hopping action is what leads to an extended Bloch wavefunction, which is the electron wavefunction in the presence of a period lattice potential. The most common way of writing a (spinless) tight-binding model in condensed matter physics would be in the second-quantization form:
where \(c_i^\dagger\) and \(c_i\) are the creation and annihilation operators for the electron at site \(i\), respectively, \(\mu_i\) are the on-site potentials, and \(J_{ij}\) are the elements of a symmetric matrix representing the hopping strengths. Moreover, \(\langle i,j\rangle\) denotes any pair of neighboring sites. To simulate this fermionic system on a gate-based quantum computer which is built on qubits, we need a similar notion for the ladder operators (i.e., creation and annihilation operators) for two-level systems: \(c^\dagger \ket{0} \to \ket{1}\) and \(c\ket{1} \to \ket{0}\). One way of representing them would be to use the Pauli gates:
Plugging them into Eq. (1) and assuming the hoppings are homogeneous such that they can be described by a single parameter \(J\), we obtain
+
\begin{equation}
+H_\text{tb}/\hbar = \sum_i \epsilon_i Z_i + J\sum_{\langle i,j\rangle}(X_i X_j + Y_i Y_j),
+\end{equation}
+where we have neglected a constant term (proportional to the identity operator) that would not have any impact on the dynamics of the system. Note that we have technically performed the inverse of the Jordan-Wigner transformation.
One of the things we care about in quantum simulations is the time-evolution of the quantum system. This is determined by the unitary operator \(e^{-iHt/\hbar}\) in quantum mechanics, where \(H\) is the time-independent Hamiltonian, which is \(H_\text{tb}\) in the case of our 1-D quantum chain. Now, to execute the time evolution unitary on a gate-based quantum computer, one must decompose it into a product of one- and two-qubit gates that can be implemented on the quantum computer. One method to accomplish this is called Trotterization, which essentially performs a discretized approximation to the continuous time evolution. To demonstrate it, let us consider a simple three-site system. The time-evolution unitary of this system is given by
+$$
+\begin{split}
+U_\text{tb}(t) &= \exp\left[\frac{-it}{\hbar}\left(H_\text{tb}^{(0,1)} + H_\text{tb}^{(1,2)}\right)\right] \\
+&\approx \left[\exp\left(\frac{-it}{n\hbar}H_\text{tb}^{(0,1)}\right) \exp\left(\frac{-it}{n\hbar}H_\text{tb}^{(1,2)}\right) \right]^n,
+\end{split}
+$$
+where in the second step we have applied Trotterization and \(n\) is the number of Trotter steps, i.e., discrete time steps. Within each two-site subsystem, the Pauli operator pairs \(X_i X_j\) and \(Y_i Y_j\) commute with each other. Therefore, with \(J = 1\) and \(\epsilon_i = 0\), we can write \(U_\text{tb}(t)\) as
+$$
+\begin{split}
+U_\text{tb}(t) &\approx \left\{\exp\left[\frac{-it}{n} \left(X_0 X_1 + Y_0 Y_1 \right) \right] \exp\left[\frac{-it}{n}\left(X_1 X_2 + Y_1 Y_2 \right) \right] \right\}^n \\
+&= \left[\exp\left(\frac{-it}{n}X_0 X_1\right) \exp\left(\frac{-it}{n}Y_0 Y_1\right) \exp\left(\frac{-it}{n}X_1 X_2\right) \exp\left(\frac{-it}{n}Y_1 Y_2\right) \right]^n \\
+&= \left[R_{X_0X_1}\left(\frac{2t}{n} \right) R_{Y_0Y_1}\left(\frac{2t}{n} \right) R_{X_1X_2}\left(\frac{2t}{n} \right) R_{Y_1Y_2}\left(\frac{2t}{n} \right) \right]^n,
+\end{split}
+$$
+where \(R_{X_iX_j}\) and \(R_{Y_iY_j}\) are parametric two-qubit \(X\otimes X\) and \(Y\otimes Y\) interaction gates between qubits \(i\) and \(j\). They are sometimes referred to as the Ising gates. Please see RXXGate and RYYGate for more details. In general, for an \(m\)-qubit system, the time-evolution unitary for each individual Trotter step can then be written as
+$$
+U_\text{tb}(\Delta t) \approx \prod_{i=0}^{m-2} R_{X_i X_{i+1}}(2\Delta t) R_{Y_i Y_{i+1}}(2\Delta t),
+$$
+where \(\Delta t = t/n\) is the discretized time step. The complete evolution over time \(t\) is then \(U_\text{tb}\approx [U_\text{tb}(\Delta t)]^{t/\Delta t}\).
+
We are now just one step shy of implementing \(U_\text{tb}(t)\) on a quantum computer, that is, to further decompose the \(R_{XX}\) and \(R_{YY}\) gates into a set of gates that are native to the quantum hardware, such as the CNOT gate and single-qubit rotation gates. For this, let us introduce another Ising gate, RZZGate \(R_{ZZ}\). It is not hard to verify that this two-qubit gate can be decomposed as a single-qubit rotation gate \(R_Z = \exp(-i\theta Z/2)\) sandwiched between two CNOT gates. Specifically, \(R_{ZZ}(\theta) = \text{CNOT}\, R_Z(\theta)\, \text{CNOT}\). Then by working out the explicit matrix representations, one can show that the \(R_{XX}\) gate has the following decomposition,
+
+
while the \(R_{YY}\) gate is decomposed as
+
+
With the above setup, we are ready to build the Trotterized quantum circuit for a general \(n\)-site quantum chain using Qiskit. First, we need to import some necessary packages.
We then define the Trot_qc function for the Trotterized quantum circuit.
+
defTrot_qc(num_qubits,t=Parameter("t")):
+"""
+ Creates the Trotterized quantum circuit at a given time for the
+ 1D tight-binding model.
+
+ Args:
+ num_qubits (int): The number of qubits in the circuit.
+ t (Parameter): time.
+
+ Returns:
+ qiskit.circuit.QuantumCircuit: The Trotterized quantum circuit.
+ """
+
+defZZ_gate(t):
+ZZ_qr=QuantumRegister(2)
+ZZ_qc=QuantumCircuit(ZZ_qr,name='ZZ')
+ZZ_qc.cnot(0,1)
+ZZ_qc.rz(2*t,1)
+ZZ_qc.cnot(0,1)
+# Convert custom quantum circuit into a gate
+ZZ=ZZ_qc.to_instruction()
+returnZZ
+
+defXX_gate(t):
+XX_qr=QuantumRegister(2)
+XX_qc=QuantumCircuit(XX_qr,name='XX')
+XX_qc.ry(np.pi/2,[0,1])
+XX_qc.append(ZZ_gate(t),[0,1])
+XX_qc.ry(-np.pi/2,[0,1])
+XX=XX_qc.to_instruction()
+returnXX
+
+defYY_gate(t):
+YY_qr=QuantumRegister(2)
+YY_qc=QuantumCircuit(YY_qr,name='YY')
+YY_qc.rx(-np.pi/2,[0,1])
+YY_qc.append(ZZ_gate(t),[0,1])
+YY_qc.rx(np.pi/2,[0,1])
+YY=YY_qc.to_instruction()
+returnYY
+
+Trot_qr=QuantumRegister(num_qubits)
+qc=QuantumCircuit(Trot_qr,name='Trot')
+
+foriinrange(num_qubits-1):
+qc.append(XX_gate(t),[Trot_qr[i],Trot_qr[i+1]])
+qc.append(YY_gate(t),[Trot_qr[i],Trot_qr[i+1]])
+
+returnqc
+
We can inspect the circuit for 3 qubits by calling the QuantumCircuit.draw() method:
+
+
Last but not least, to see if we have implemented Trotterization correctly, we may compute the process fidelity between the Trotterized quantum channel and the exact time-evolution unitary. We define the exact time-evolution unitary as follows:
+
fromqiskit.opflowimportI,X,Y,Z
+
+defU_tb(t):
+"""
+ Exact time-evolution unitary for 3 qubits.
+ """
+# Interactions (I is the identity matrix; X and Y are Pauli matricies;
+# ^ is a tensor product)
+XXs=(I^X^X)+(X^X^I)
+YYs=(I^Y^Y)+(Y^Y^I)
+H_tb=XXs+YYs
+return(t*H_tb).exp_i()
+
On the other hand, the approximate unitary based on Trotterization is constructed as follows:
+
defU_trot_tb(t_target,trotter_steps,num_qubits):
+"""
+ Creates the Trotterized time-evolution unitary for a
+ 1-D tight-binding model.
+
+ Args:
+ t_target (float): The total time evolved.
+ trotter_steps (int): The number of Trotter steps.
+ num_qubits (int): The number of qubits in the circuit.
+
+ Returns:
+ qiskit.quantum_info.Operator: The operator corresponding to
+ the Trotterized time-evolution unitary.
+ """
+
+t=Parameter("t")
+qr=QuantumRegister(num_qubits)
+qc=QuantumCircuit(qr)
+
+for_inrange(trotter_steps):
+qc.append(Trot_qc(num_qubits,t),qr)
+qc=qc.bind_parameters({t:t_target/trotter_steps})
+returnqi.Operator(qc)
+
Finally, we can plot how the Trotter error (= 1 - process fidelity) changes as we increase the number of Trotter steps.
Yay 🎉! The trotter error decreases as the number of Trotter steps increases, suggesting that we have indeed implemented Trotterization correctly.
+
So here comes the end of the first part. In the next post, we will see how we can use the Trotterized quantum circuit that we just built to study phenomena including the quantum random walk and Anderson localization on a 1-D quantum chain. Stay tuned!
As promised in the
+
+ previous blog post, we will now continue our journey into the world of many-body physics simulations with quantum computers. In this post, we want to address the question of how a particle such as an electron, propagates through an intrinsically quantum system, i.e., the 1-D quantum chain that we built previously. Remember everything we discussed so far has not taken into account disorder that is generally present in realistic condensed matter systems. Disorder is something that generically breaks some symmetries of the system Hamiltonian and/or leads to deviations from the lattice periodicity. When disorder is present, all the sites in a lattice are no longer equivalent. So another question naturally arises: how would disorder affect the quantum transport? The exploration of this general question has led to wonderful discoveries of various localization effects in disordered systems. Here we will touch on one of them, Anderson localization, which was first discovered by the great physicist Philip W. Anderson in 1958 [1]. One interesting thing to note is that the dimensionality of a system has a direct impact on Anderson localization. For example, according to the scaling theory of localization [2], in 1- and 2-D, a system will be a perfect insulator in the thermodynamic limit (simply put, when the system size is taken to infinity). Therefore, Anderson localization must happen in one- and two-dimensional disordered systems regardless of the disorder strength! However, in 3-D, Anderson localization is a critical phenomenon where the system undergoes a metal-insulator transition (MIT). This means that localization happens only when the disorder strength exceeds a certain threshold. Even though this is certainly one of the most interesting aspects of Anderson localization, we will not explore it here for the sake of simplicity. We will again stick to the 1-D system as in the previous post.
Classical random walk is a random process that is prevalent in many phenomena in nature, such as the motion of macroscopic particles in liquids and gases known as Brownian motion and even the price of a fluctuating stock. In the most rudimentary version of a symmetric classical random walk on a lattice, at each time step, the probabilities of the particle jumping to any of the neighboring sites are the same. In 1-D, this means that the particle can move one site to the left or right with equal probability (50%) each time. A well-established result for a symmetric random walk is that in the continuous-time limit, the probability of finding the particle at time \(t\) at position \(r\) (from the origin) follows a Gaussian distribution:
where we have assumed the distance of each jump to be 1. Therefore, the standard deviation of this probability distribution scales as \(\sigma_\text{classical}\propto \sqrt{t}\). Since \(\langle r \rangle = 0\), this suggests that the mean-square-root displacement of the particle, which quantifies the spatial propagation of the particle relative to the origin, also scales diffusively with time as \(\sqrt{\langle r^2\rangle} \propto \sqrt{t}\).
+
Since we are interested in particle propagation in a quantum system, we will need to deal with quantum random walks instead. In this case, the intrinsic quantum nature including superposition and interferences among different wavefunctions will lead to a qualitative difference from the classical counterpart. Here is a somewhat intuitive way to think about the difference between them. Imagine a “classical walker” who decides whether to step left or right by tossing a coin with two possible outcomes \(+\) and \(-\), with probabilities \(P_+\) and \(P_-\), respectively. After each toss, they would look at the result and decide which way to go. So the classical random walk traces a single path within a decision tree. In contrast, a “quantum walker” flips their coin but never looks at the outcome. Instead, at each step, they step simultaneously to the left and right with some complex amplitudes \(A_+\) and \(A_-\) corresponding to probabilities \(P_+ = \lvert A_+\rvert^2\) and \(P_- = \lvert A_-\rvert^2\). After many iterations the quantum random walk results in an extended wavefunction of the quantum walker that spreads out to all positions in the tree with finite amplitudes. What’s more incredible is that these complex amplitudes at different sites add up for any given path and depending on the phase differences, this creates constructive or destructive interferences when measuring the probabilities at the end. A diagram illustrating the ideas above is shown below (taken from [3]).
+
+
Due to the critical differences highlighted above, it can be shown that a symmetric quantum random walk in the continuous-time limit will lead to a probability distribution that follows a Bessel function of the first kind[4]:
Below is a visualization of the two probability distributions for continuous-time classical and quantum random walks:
+
+
Moreover, a quantum random walk exhibits ballistic propagation with the mean-square-root displacement scaling linearly with time, \(\sqrt{\langle r^2\rangle} \propto t\). The quadratic speed-up of quantum random walks versus classical random walks is analogous to the quadratic speed-up of the Grover search algorithm compared to a classical search!
Let us now simulate the quantum random walk on a 1-D tight-binding chain to see if the result matches the theory prediction above. We will first do this on a simulator and then on a real quantum computer. For this simulation, we consider a 5-site tight-binding lattice whose Hamiltonian is given by
Again, we have set the on-site potentials to be zero to simulate a clean system without disorder and will use \(J = 1\) from here onwards [cf. Eq. (2) in
+
+ part I]. Recall that to simulate the dynamics of a quantum system on a gate-based quantum computer, we employ the Trotterization process to discretize the continuous time evolution. Therefore, we use the Trot_qc function established in
+
+ part I to build the Trotterized quantum circuit for this 5-site system at any given time. Next, we want to add a particle in the form of an excitation to site 0, i.e., qubit 0. This is done by apply an \(X\) gate to flip the qubit from \(\ket{0}\) state to \(\ket{1}\) state. We modify the U_trot_tb function in the previous part slightly to record the Trotterized circuits at all Trotter (time) steps over a given simulation time:
+
defU_trot_circuits(delta_t,trotter_steps,num_qubits):
+"""
+ Record a list of Trotterized circuits at all Trotter steps
+ separated by delta_t.
+
+ Args:
+ delta_t (float): Duration of individual time steps.
+ trotter_steps (array): Array of intermediate time steps.
+ num_qubits (int): The total number of qubits.
+
+ Returns:
+ circuits (list): A list of Trotterized quantum circuits.
+ """
+
+t=Parameter("t")
+circuits=[]
+
+forn_stepsintrotter_steps:
+qr=QuantumRegister(num_qubits)
+cr=ClassicalRegister(num_qubits)
+qc=QuantumCircuit(qr,cr)
+
+qc.x(0)# Add an excitation to site 0
+
+for_inrange(n_steps):
+qc.append(Trot_qc(num_qubits,t).to_instruction(),qr)
+qc=qc.bind_parameters({t:delta_t})
+circuits.append(qc)
+returncircuits
+
We can now track the propagation of the particle by keeping track of the probability of finding it on each qubit at different time steps. To simulate this process with a simulator, we will make use of the statevector_simulator backend in Qiskit’s Aer module. For this, we make some additional imports here.
Here we define a function that extracts the probabilities of each qubit being in the \(\ket{1}\) state at different times using the output state from the statevector_simulator.
+
defprobability_density(delta_t,trotter_steps,num_qubits):
+"""
+ Calculate the probabilities of finding the excitation
+ on each qubit at different time steps.
+
+ Args:
+ delta_t (float): Duration of individual time steps.
+ trotter_steps (array): Array of intermediate times.
+ num_qubits (int): The total number of qubits.
+
+ Returns:
+ probability_density (array): The probability density of
+ the excitation at all Trotter steps.
+ """
+
+backend_sim=Aer.get_backend('statevector_simulator')
+circuits=U_trot_circuits(delta_t,trotter_steps,num_qubits)
+probability_density=[]
+
+forcircincircuits:
+transpiled_circ=transpile(circ,backend_sim,optimization_level=3)
+job_sim=backend_sim.run(transpiled_circ)
+
+# Grab the results from the job.
+result_sim=job_sim.result()
+outputstate=result_sim.get_statevector(transpiled_circ,decimals=5)
+
+ps=[]
+# Extract the probability of finding the excitation on each qubit.
+# (e.g. for 5 qubits, we need "00001", "00010", "00100", "01000", "10000")
+foriinrange(num_qubits):
+ps.append(np.abs(outputstate[2**i])**2)
+probability_density.append(ps)
+probability_density=np.array(probability_density)
+returnprobability_density
+
It’s time to simulate the quantum random walk on the 5-site tight-binding lattice and see the results. We choose to evolve the system over 25 steps with a step size of 0.15 for this simulation. We can visualize the results by running the following code,
This result is pretty nice as it shows that the particle excitation traverses the lattice and eventually reflects off the opposite end of the 1-D chain. In principle, the quantum random walk propagates in both directions for all the interim sites. However, quantum interferences among these multiple trajectories alter the particle wavepacket as it evolves in time, leading to the unidirectional transport seen above. Furthermore, it is evident that the displacement of the excitation scales linearly with time, agreeing well with the prediction of quantum random walks.
+
Performing the same simulation on a real quantum computer is slightly different. Here we can no longer retrieve the statevectors directly from the outputs. Instead, at each time step we will need to send the circuit to the quantum computer many times (specified by the number of shots) for execution and extract the probabilities based on the output statistics. During the Challenge, we were given the access to one of IBM’s 7-qubit systems ibm_nairobi. We can perform the simulation by calling the following function:
+
defprobability_density_exp(delta_t,trotter_steps,num_qubits,shots):
+# Load your IBM Quantum account
+IBMQ.load_account()
+# Get the backend
+provider=IBMQ.get_provider(hub=<hub_name>,group=<group_name>,project=<project_name>)
+backend=provider.get_backend('ibm_nairobi')
+
+# Create transpiled circuits for hardware execution
+initial_layout=[0,1,3,5,4]# Specific to ibm_nairobi topology
+hardware_transpiled_circuits=[]
+
+forcircinU_trot_circuits(delta_t,trotter_steps,num_qubits):
+hardware_circ=deepcopy(circ)
+hardware_circ.measure(range(num_qubits),range(num_qubits))
+hardware_transpiled_circuits.append(
+transpile(hardware_circ,backend,initial_layout=initial_layout,optimization_level=3)
+)
+
+# Run the circuits
+job=execute(hardware_transpiled_circuits,backend=backend,shots=shots)
+exp_results=job.result()
+
+probability_density_exp=[]
+foroutputinexp_results.get_counts():
+ps=[]
+# Extract the probabilities
+keys=['00001','00010','00100','01000','10000']
+forkeyinkeys:
+ifkeyinoutput:
+ps.append(output[key]/shots)
+else:
+ps.append(0.)
+
+probability_density_exp.append(ps)
+returnprobability_density_exp
+
Below is the result for one of the simulations on the hardware.
+
+
Comparing this with the result above from the simulator, despite the similarity, the effect of noise and decoherence (especially at later times) on a real quantum hardware is apparent!
Finally, we are coming to Anderson localization. As mentioned before, Anderson localization always happens in 1-D systems when disorder is present. Lattice inhomogeneity causes scattering and leads to quantum interference that tends to inhibit particle propagation, a signature of localization. The wavefunction of a localized particle rapidly decays away from its initial position, effectively confining the particle to a small region of the lattice. This localization phenomenon is a direct consequence of interference between different paths arising from multiple scatterings of the electron by lattice defects. To study this phenomenon, we add back the inhomogeneous on-site potentials to the Hamiltonian, thereby making the lattice sites inequivalent, i.e.,
One simple way to model disorder within the tight-binding system is through the Aubry-Andre (AA) model, where the disorder is replaced by a periodic modulation of the on-site energies, with a spatial period incommensurate with the lattice period. The AA potential is modeled as \(\epsilon_i = W\cos(2\pi\beta i)\), where \(\beta\) determines the quasicrystal periodicity and \(W\) is the disorder strength. Moreover, with the addition of the on-site terms, we also need to modify the Trotterized circuit. Note that exponentiating the \(Z_i\) gates for the time-evolution unitary simply leads to \(R_{Z_i}\) gates acting on individual qubits. So we can define a Trot_qc_disorder circuit based on the Trot_qc circuit from
+
+ part 1:
+
defTrot_qc_disorder(num_qubits,t,deltas):
+"""
+ Generate a Trotterized quantum circuit with disorder.
+
+ Args:
+ num_qubits (int): The total number of qubits.
+ t (Parameter): The time parameter.
+ deltas (list(Parameter)): The list of disorder parameters.
+
+ Returns:
+ qiskit.QuantumCircuit: The Trotterized quantum circuit with disorder.
+ """
+
+Trot_qr_disorder=QuantumRegister(num_qubits)
+Trot_qc_disorder=QuantumCircuit(Trot_qr_disorder,name='Trot disorder')
+
+Trot_gate=Trot_qc(num_qubits,t).to_instruction()
+Trot_qc_disorder.append(Trot_gate,Trot_qr_disorder)
+foriinrange(num_qubits):
+Trot_qc_disorder.rz(2*deltas[i]*t,i)
+returnTrot_qc_disorder
+
Let us then define a function that records the Trotterized circuits with finite disorder at all Trotter (time) steps:
+
defU_trot_circuits_disorder(delta_t,trotter_steps,num_qubits,W,beta):
+"""
+ Record a list of Trotterized quantum circuits with disorder
+ at all Trotter steps.
+
+ Args:
+ delta_t (float): Duration of individual time steps.
+ trotter_steps (array): Array of intermediate times.
+ num_qubits (int): The total number of qubits.
+ W (float): The disorder strength.
+ beta (float): The quasicrystal periodicity of the AA model.
+
+ Returns:
+ disorder_circuits (list): List of Trotterized quantum circuits with disorder.
+ """
+
+t=Parameter('t')
+deltas=[Parameter('delta_{:d}'.format(idx))foridxinrange(num_qubits)]
+
+AA_pattern=np.cos(2*np.pi*beta*np.arange(num_qubits))
+disorders=W*AA_pattern
+disorder_circuits=[]
+
+forn_stepsintrotter_steps:
+qr=QuantumRegister(num_qubits)
+cr=ClassicalRegister(num_qubits)
+qc=QuantumCircuit(qr,cr)
+
+qc.x(0)
+
+for_inrange(n_steps):
+qc.append(Trot_qc_disorder(num_qubits,t,deltas),qr)
+
+qc=qc.bind_parameters({t:delta_t})
+qc=qc.bind_parameters({deltas[idx]:disorders[idx]foridxinrange(num_qubits)})
+disorder_circuits.append(qc)
+returndisorder_circuits
+
Like in the previous section, we will simulate the particle propagation with disorder on a simulator. Here is how it looks like:
+
+
Comparing with the quantum random walk result, it is clear that in the presence of the AA disorder, the particle tends to be localized in its initial position (qubit 0) as time evolves. So we successfully see the effect of Anderson localization in this 1-D system! Again, running the same simulation on a quantum computer we see a degradation in quality of the results due to noise, but we can still reach the same conclusion in this case:
It’s been a long post to get to this point, but just to conclude, we have successfully simulated the particle propagation in a 1-D quantum chain with and without disorder on both a simulator and a real quantum computer provided by IBM. In the case of no disorder, we saw behaviors of a quantum random walk, while is distinct from a classical random walk. In the presence of disorder, we saw the effect of Anderson localization, i.e., the particle tends to localize in its initial position over time. In the next and final post of this series, we will look into a more complex example of localization beyond the single-particle picture we have been adhering to so far, that is, many-body localization. The question there is: does localization still happen when we take into account particle interactions? See you in the next one!
This is the third and final blog post of the 2022 IBM Spring Challenge series where I introduce some basic concepts and implementations of quantum simulation of many-body physics. Just a quick recap here: In the first post we set up the quantum system that we wanted to investigate using a 1-D tight-binding model, and we discussed how to utilize the Trotterization procedure to implement the time-evolution of the system on a gate-based quantum computer. In the second post, using the tools developed previously, we simulated the propagation of one particle excitation on a 1-D quantum chain with and without disorder on both a simulator and a real quantum computer. We saw the behavior of a quantum random walk when there is no disorder and Anderson localization when disorder is present. We emphasize that the tight-binding model that we have been working with so far does not include particle-particle interactions. So Anderson localization is really just a single-particle, i.e., non-interacting, effect. Then naturally we are prompted to ask the question of what happens when we do take interactions into account. This leads to the rich physics of many-body localization (MBL) that we will touch on in this post. Please feel free to check out
+
+ part I and
+
+ II before continuing.
To talk about many-body localization, it is beneficial to first introduce the concept of thermalization, or thermal equilibrium, following Ref. [1]. Thermalization in a closed classical system hinges on the powerful ergodic hypothesis, which states that over a long period of time all microstates of the system are accessed with equal probability. However, this notion of ergodicity does not directly apply to quantum systems. Here is a simple example to see why. Let us consider an isolated quantum many-body system with Hamiltonian \(H\). The generic initial non-equilibrium state \(\ket{\psi(0)}\) can be expanded over the basis of many-body eigenstates \(\ket{\alpha}\) as \(\ket{\psi(0)} = \sum_\alpha A_\alpha \ket{\alpha}\). After the quantum evolution over an arbitrarily long time \(t\), the state becomes
where \(E_\alpha\) is the energy of the eigenstate \(\ket{\alpha}\). Then the probability of finding the system in a given eigenstate \(\ket{\alpha}\), \(p_\alpha = \lvert A_\alpha\rvert^2\), is set by the choice of the initial state and does not change over time. So unlike the classical case, the time evolution of a quantum system does not uniformly sample all states in the Hilbert space. Therefore, for quantum ergodicity, we should instead demand that starting from a generic initial state the system’s observables (few-body operators) settle to values given by the microcanonical ensembles at sufficiently long times. The infinite-time average of a physical observable \(O\) is given by
Since \(p_\alpha\) are fixed by the initial state, the natural way to ensure that an observable \(O\) reaches a thermal expectation value at long times for generic initial states is to assume that the expectation values in individual eigenstates \(\langle\alpha\vert O\vert \alpha\rangle\) agree with the microcanonical ensemble. This is the essence of the eigenstate thermalization hypothesis (ETH), which is an important concept in this subject. More precisely, the ETH states that in ergodic systems individual many-body eigenstates have thermal observables that are identical to microcanonical ensemble values at energy \(E = E_\alpha\), i.e., \(\langle\alpha\vert O\vert \alpha\rangle \approx \langle O\rangle_\text{mc}(E)\). The microcanonical ensemble average can be written as
where \(N(E, \Delta E)\) is the number of eigenstates in the system that are within \(\Delta E\) of energy \(E\).
+
After a fairly long digression, rather than diving further into the glory details of the ETH and its implications, we will come back to, in some sense, the other end of the spectrum, MBL, which is known to violate the ETH and hence does not thermalize. Generally speaking, thermalization requires that different parts of ergodic systems exchange energy and particles, and consequently leads to conduction. On the other hand, localization leads to the absence of diffusion, suppressing transport. We have seen the example of Anderson localization, where a disorder potential can completely change the nature of single-particle eigenstates in a non-interacting system. However, interactions between particles are inevitable in realistic systems. It is conceivable that interactions may open up new transport channels, e.g., a high-energy localized state may decay to produce excitations at lower energies, potentially restoring transport. Therefore, to understand the fate of localization in the presence of particle interaction is not a trivial problem. In particular, Basko, Aleiner, and Altshuler (BAA) first studied the stability of the Anderson insulator against short-ranged interactions and concluded that the interacting model enters a localized phase termed the MBL phase in arbitrary dimensions below a certain critical temperature [2]. Later, by studying a 1-D disordered fermionic chain, it was pointed out that the MBL phase can persist even at infinite temperature [3]. More recent developments however challenged the conclusion of BAA. It was argued that in high dimensions \(d>1\) small thermal inclusions can trigger avalanches in the system that destroy the MBL phase [4, 5]. The reconciliation of these two results is still an open problem.
Following the history of this field, the introduction of the fermionic and spin-chain lattice models has definitely opened the door to studying many interesting properties of MBL in (classical) numerical simulations. This is exactly the system we have built up in the previous parts of the Challenge. Here we will do something different from the setup in the original IBM Challenge Problem 3. On top of the tight-binding model used to study Anderson localization in
+
+ part II, we will add an additional \(ZZ\)-interaction term. So the tight-binding Hamiltonian is given by
where \(n\) is the number of sites in the 1-D chain. Readers with a condensed matter physics background may recognize that this is exactly the so-called Heisenberg XXZ model. In fact, the Heisenberg model has become the paradigmatic model for studying MBL physics (among many other phenomena). The reason for the additional term should be obvious by doing the Jordan-Wigner transformation, which maps the spin-chain Hamiltonian above to a spinless fermionic chain in the second-quantization form given by
where \(n_i \equiv c_i^\dag c_i\) is the density operator for site \(i\). Hence, we see that the additional \(U\)-term gives rise to the two-body interaction between neighboring sites. Without it, the tight-binding model is a free-fermion model, which is suited for the study of Anderson localization but not many-body localization. So here we will simulate the MBL phase in a disordered Heisenberg XXZ spin chain. We will again set \(J = 1\) and the model thus has two free parameters: the interaction strength \(U\) and the disorder strength \(W\). Recall that \(W\) controls the onsite potentials through the Aubry-Andre model, \(\epsilon_i = W\cos(2\pi\beta i)\), with \(\beta\) being related to the quasicrystal periodicity.
+
One interesting aspect of the 1-D Heisenberg XXZ model is its phase diagram. In the limit \(U\to 0\) with some finite \(W\), i.e., when there is no interaction, this model is equivalent to free fermions moving in a disordered potential and therefore, the states are Anderson localized. Turning on the interaction will result in the MBL phase. Furthermore, for fixed and not very strong disorder (\(W/U \sim 1\)), it was found that tuning the interaction strength \(U\) above some critical value \(U^\ast\) will lead to delocalization. On the other hand, if we fix \(U\) and increase the disorder strength \(W\), we will see a transition from a delocalized (thermal) phase to the MBL phase when \(W\) goes above a critical value \(W^\ast\). Therefore, in 1-D interacting systems there exists a metal-insulator transition, which is distinct from non-interacting systems that always Anderson localize in the thermodynamic limit, as mentioned in the
+
+ previous post. A schematic illustrating the phase diagram of the XXZ spin chain is shown below.
Okay, enough theory. Let’s actually build the quantum circuit for the Heisenberg model and simulate its dynamics. In this case, we will simulate three particles in a 12-site spin chain, where particle excitations are represented by \(\ket{1}\) and empty sites represented by \(\ket{0}\). Like before, to better visualize (de)localization, we would like to track the probability of each site being in the \(\ket{1}\) state over the course of the entire Trotterized time evolution. In addition, we also keep track of two other quantities. One is the imbalance, which is one of the signatures of the breakdown of thermalization. The system imbalance is defined as
where \(N_e\) and \(N_o\) are the populations at the even and odd sites of the system, respectively, and the expectation value is defined with respect to a particular quantum state, i.e. \(\langle \cdots \rangle = \langle \psi \lvert \cdots \rvert \psi \rangle\). In a thermalized system, we expect each site of the lattice to be occupied by the same average number of particles after reaching steady state. Therefore, the imbalance is close to zero. However, when localization happens, we should expect a deviation from zero. Here we define a function that calculates the imbalance for a given state:
+
defget_imbalance(state):
+"""
+ Calculate the imbalance of a state.
+
+ Args:
+ state (qiskit.quantum_info.Statevector): The state vector.
+
+ Returns:
+ imbalance_val (float): The imbalance of the state.
+ """
+imbalance_val=0
+state_dict=state.to_dict()
+
+forbasis,ampinstate_dict.items():
+Ne,No=0,0
+# Make sure to skip calculating the |00...0> state
+foriinrange(len(basis)):
+ifi%2==0andbasis[i]=='1':
+Ne+=1
+elifi%2==1andbasis[i]=='1':
+No+=1
+ifNe+No!=0:
+imbalance_val+=np.abs(amp)**2*(Ne-No)/(Ne+No)
+returnimbalance_val
+
The other quantity to probe is the entanglement entropy formed in the lattice as a result of particle propagation. One can imagine that while the created particle excitations are initially separable from the the rest of the lattice, their propagation will lead to the creation and distribution of entanglement throughout the lattice. For simplicity, we will only keep track of the entanglement entropy of part of the system, say the first lattice site. This can be quantified by its von Neumann entropy, which is defined as
where \(A\) refers to the subsystem of interest (i.e., the first lattice site), and \(\rho_A = \text{tr}_{B}\rho\) is the reduced density matrix of the subsystem \(A\) after “tracing out” the rest of the system which we call \(B\). If the subsytem \(A\) is fully entangled with the rest of the system, \(\mathcal S _\text{vn}(\rho_A) = \ln 2\), whereas if the subsytem is completely separable with respect to the rest, \(\mathcal S _\text{vn}(\rho_A) = 0\). One can probe entanglement at a larger scale by using more sophisticated measures such as the concurrences and global entanglement, but we will not pursue these here.
+
Let us now get straight into the simulation. Some necessary imports here:
Similar to what was done in
+
+ part I, we will first define the Trotterized quantum circuit for MBL based on the tight-binding Hamiltonian Eq. (1) above.
+
defTrot_qc_mbl(num_qubits,t,J,deltas):
+"""
+ Creates the Trotterized quantum circuit at a given time for
+ the 1-D Heisenberg XXZ model.
+
+ Args:
+ num_qubits (int): The number of qubits in the circuit.
+ t (Parameter): time.
+ J (Parameter): The interaction strength.
+ deltas (List[Parameter]): The list of the disorder
+ parameters.
+
+ Returns:
+ qiskit.circuit.QuantumCircuit: The Trotterized
+ quantum circuit.
+ """
+
+defZZ_gate(J,t):
+ZZ_qr=QuantumRegister(2)
+ZZ_qc=QuantumCircuit(ZZ_qr,name='ZZ')
+ZZ_qc.cnot(0,1)
+ZZ_qc.rz(2*J*t,1)
+ZZ_qc.cnot(0,1)
+# Convert custom quantum circuit into a gate
+ZZ=ZZ_qc.to_instruction()
+returnZZ
+
+defXX_gate(t):
+XX_qr=QuantumRegister(2)
+XX_qc=QuantumCircuit(XX_qr,name='XX')
+XX_qc.ry(np.pi/2,[0,1])
+XX_qc.append(ZZ_gate(1,t),[0,1])
+XX_qc.ry(-np.pi/2,[0,1])
+XX=XX_qc.to_instruction()
+returnXX
+
+defYY_gate(t):
+YY_qr=QuantumRegister(2)
+YY_qc=QuantumCircuit(YY_qr,name='YY')
+YY_qc.rx(-np.pi/2,[0,1])
+YY_qc.append(ZZ_gate(1,t),[0,1])
+YY_qc.rx(np.pi/2,[0,1])
+YY=YY_qc.to_instruction()
+returnYY
+
+Trot_qr=QuantumRegister(num_qubits)
+qc=QuantumCircuit(Trot_qr,name='Trot')
+
+foriinrange(num_qubits-1):
+qc.append(XX_gate(t),[Trot_qr[i],Trot_qr[i+1]])
+qc.append(YY_gate(t),[Trot_qr[i],Trot_qr[i+1]])
+qc.append(ZZ_gate(J,t),[Trot_qr[i],Trot_qr[i+1]])
+
+foriinrange(num_qubits):
+qc.rz(2*deltas[i]*t,i)
+
+returnqc
+
Next we define the function that records the Trotterized circuits at all time steps. Note that we will start with 3 particle excitations at sties 0, 4, and 8.
+
defU_trot_circuits_mbl(delta_t,trotter_steps,num_qubits,U,W,beta):
+"""
+ Record a list of Trotterized quantum circuits for many body localization
+ at all Trotter steps.
+
+ Args:
+ delta_t (float): Duration of individual time steps.
+ trotter_steps (array): Array of intermediate times.
+ num_qubits (int): The total number of qubits.
+ U (float): The interaction strength.
+ W (float): The disorder strength.
+ beta (float): The quasicrystal periodicity of the AA model.
+
+ Returns:
+ disorder_circuits (list): List of Trotterized quantum
+ circuits for MBL.
+ """
+
+t=Parameter('t')
+J=Parameter('J')
+deltas=[Parameter('delta_{:d}'.format(idx))foridxinrange(num_qubits)]
+
+
+AA_pattern=np.cos(2*np.pi*beta*np.arange(num_qubits))
+disorders=W*AA_pattern
+mbl_circuits=[]
+
+forn_stepsintrotter_steps:
+qr=QuantumRegister(num_qubits)
+cr=ClassicalRegister(num_qubits)
+qc=QuantumCircuit(qr,cr)
+
+qc.x([0,4,8])# three particle excitations
+
+for_inrange(n_steps):
+qc.append(Trot_qc_mbl(num_qubits,t,J,deltas),qr)
+
+qc=qc.bind_parameters({t:delta_t})
+qc=qc.bind_parameters({deltas[idx]:disorders[idx]foridxinrange(num_qubits)})
+qc=qc.bind_parameters({J:U})
+mbl_circuits.append(qc)
+returnmbl_circuits
+
We first fix the value of the interaction strength \(U = 1.0\) and simulate the system at four different values of disorder strength \(W = [0.2,\ 2,\ 4,\ 8]\).
We can then simulate these Trotterized circuits on Qiskit’s statevector_simulator backend and track the time evolution of the probability of finding a particle for all the lattice sites, the imbalance of the system, and the von Neumann entropy of the first site:
+
backend_sim=Aer.get_backend('statevector_simulator')
+
+probability_densities={}
+state_vector_imbalances={}
+vn_entropies={}
+
+forWintqdm(Ws):
+probability_densities[W]=[]
+state_vector_imbalances[W]=[]
+vn_entropies[W]=[]
+
+forcircincircuits[W]:
+
+transpiled_circ=transpile(circ,backend_sim,optimization_level=3)
+job_sim=backend_sim.run(transpiled_circ)
+# Grab the results from the job.
+result_sim=job_sim.result()
+outputstate=result_sim.get_statevector(transpiled_circ,decimals=6)
+
+# extract the probability densities
+ps=[]
+foridxinrange(num_qubits):
+ps.append(np.abs(qi.partial_trace(outputstate,[iforiinrange(num_qubits)ifi!=idx]))[1,1]**2)
+
+# extract the density matrix of qubit 0 by tracing out all other qubits
+entropy=0
+rho_0=qi.partial_trace(outputstate,range(1,num_qubits))
+entropy+=qi.entropy(rho_0,base=np.exp(1))
+
+# calculate the imbalance of the system
+imbalance=0
+imbalance+=get_imbalance(outputstate)
+
+vn_entropies[W].append(entropy)
+probability_densities[W].append(ps)
+state_vector_imbalances[W].append(imbalance)
+
Below we show the results of this simulation. First is the probability densities at different disorder strengths.
+
+
We see that with weak disorder the system is delocalized but as \(W\) increases, MBL kicks in and the particles are localized around their initial positions over time. Next we look at the imbalance as a function of time.
+
+
Again, as expected, when \(W\) is small, the system tends to thermalize and the average imbalance is close to 0. But at strong disorder, we see a large deviation from 0, indicated by the green and red curves in the plot. Finally, here is the result of the von Neumann entropy.
+
+
We see the average entanglement entropy over time decreases as the disorder strength increases since the system is transitioning into the MBL phase.
+
As the final simulation, we will fix the disorder strength \(W = 3.5\) and vary the interaction strength \(U = [0.2,\ 1,\ 3,\ 5]\) to see how the behavior of the system changes. The execution is similar to the one above, so I won’t bore you with basically the same codes again. Let us directly look at the results.
+
+
+
+
Based on the phase diagram discussed in the previous section, we expect that the system will transition from MBL to delocalization as \(U\) increases, when the disorder is not very strong. This can be observed in the probability densities, where the sign of delocalization can be seen starting from \(U = 3\), especially on the first qubit. This is also supported by considering the average imbalance and entanglement entropy at different interaction strengths. Although it is not as clear as the previous case with a fixed \(U\) and varying \(W\), we can still see that overall the magnitude of imbalance is smaller when the system becomes delocalized (i.e., with larger \(U\)), while the entanglement entropy gets larger. These simulation results agree with the theoretical expectations.
Here we conclude the quantum simulations of many-body localization as well as the blogging about the IBM Quantum Spring Challenge 2022 as a whole. We have explored parts of the phase diagram of a 1-D disordered Heisenberg XXZ chain. We saw the somewhat competing effect between particle interaction and disorder, leading to transition between thermal and MBL phases. Our quantum simulations have successfully captured some of the key features of this phase diagram. However, it is fair to say that the simulations showcased here barely scratch the surface of the fascinating physics of MBL and non-equilibrium quantum systems as a whole. There are other profound topics of the MBL systems which we did not touch upon and some of them are under active investigation, including the emergent integrability and even a whole new class of MBL-protected phases of matter.
+
With that, I want to thank you for taking the time to follow along and I hope you enjoyed this journey into quantum simulations of many-body physics as much as I did!
Title: Impact of the Fermi Arc Diversity on the Berry Curvature Dipole in Time Reversal Invariant Weyl Semimetals
+
Authors: Diego García Ovalle - Armando Pezo - Aurélien Manchon (Aix-Marseille Université, CNRS, CINaM, Marseille, France.)
+
Abstract:
+
Whereas anomalous Hall effect has been thought to only exist in materials presenting a net magnetization, such as ferro- or ferrimagnetic metals, recent progress has revealed that this is not the case. In particular, it has been recently proposed that in nonmagnetic crystals a transverse Hall current can develop at the second order in the electric field. This effect, tagged the nonlinear Hall effect, arises in certain non-centrosymmetric crystals and is driven by the Berry curvature dipole. Weyl Semimetals are particularly promising platforms for the observation of the nonlinear Hall effect because their bulk Fermi surface is composed of Weyl nodes with diverging Berry curvature. Nonetheless, Weyl Semimetals display another intriguing aspect that has remained scarcely addressed so far. Depending on the Weyl cone inclination, their surface feature a wide variety of trivial and non-trivial states, including Fermi pockets, arcs and track states.
+
In this work, adopting a model for noncentrosymmetric Weyl Semimetal, we investigate the impact of these surface states on the nonlinear Hall response. We show that depending on the slab geometry, surface states can have a dramatic impact on the nonlinear Hall effect, resulting in substantial thickness-dependence. We also extend our study to the realistic case of WTe2 thin film using a Wannier-projected tight-binding representation. Finally, we mention the intimate connection that is expected between the nonlinear Hall effect and the Orbital Edelstein Effect of these noncentrosymmetric materials.
Starting from Spring 2022, I am co-organizing a Condensed Matter Physics (CMP) Journal Club at CWRU. Currently we are having biweekly meetings where two speakers will present their papers of choice. We are hosting the meetings both in-person and over Zoom. If you are interested in joining or want to present a recent CMP paper of your own, please do not hesitate to contact me for more details.
My current research interests are focused on leveraging near-term quantum computers to solve problems in various domains, which include but are not limited to:
+
+
Simulation of many-body systems: creating exotic phases of matter, simulating phase transitions, improving techniques for Hamiltonian simulation with the help of machine learning, etc.
+
Optimization: exploring quantum optimization algorithms, such as the Quantum Approximate Optimization Algorithm (QAOA), Variational Quantum Eigensolver (VQE), etc. and their applications in life science problems such as protein folding and drug discovery
+
Machine learning: developing and benchmarking quantum machine learning algorithms for electronic health records, medical imaging, etc.
+
+
Things I have worked on in the past:
+
+
Topological materials: investigating novel spin/charge transport phenomena in topological semimetals, such as Weyl and Dirac semimetals
+
Dark matter models: studying the phenomenology of dark matter models in theories with a gauged baryon symmetry
+
+
+
+
+
+
+
+
+
+
+
+
+
+ I am always open to collaborations and discussions. Please feel free to reach out to me via the links in my
+
+ homepage.
+