Quantum simulation
De Seitn is no ned ibersetzt worn. Sie schaung de englische Originalversion o.
Yukio Kawashima (May 30, 2024)
Download the pdf of the original lecture. Note that some code snippets might become deprecated since these are static images.
Approximate QPU time to run this experiment is 7 seconds.
(This notebook is mostly taken from a now-deprecated tutorial notebook for Qiskit Algorithms.)
1. Introduction
As a real time evolution technique, Trotterization consists in the successive application of a quantum gate or gates, chosen to approximate the time evolution of a system for a time slice. Following from the Schrödinger equation, the time evolution of a system initially in the state takes the form:
where is the time-independent Hamiltonian governing the system. We consider a Hamiltonian that can be written as a weighted sum of Pauli terms , with representing a tensor product of Pauli terms acting on qubits. In particular, these Pauli terms might commute with one another, or they might not. Given a state at time , how do we obtain the system's state at a later time using a quantum computer? The exponential of an operator can be most easily understood through its Taylor series:
Some very basic exponentials, like can be implemented easily on quantum computers using a compact set of quantum gates. Most Hamiltonians of interest will not have just a single term, but will instead have many terms. Note what happens if :
When and commute, we have the familiar case (which is also true for numbers, and variables and below):
But when operators do not commute, terms cannot be rearranged in the Taylor series to simplify in this way. Thus, expressing complicated Hamiltonians in quantum gates is a challenge.
One solution is to consider very small time , such that the first-order term in the Taylor expansion dominates. Under that assumption:
Of course, we might need to evolve our state for a longer time. That is accomplished by using many such small steps in time. This process is called Trotterization:
Here is the time slice (evolution step) that we are choosing. As a result, a gate to be applied times is created. A smaller timestep leads to a more accurate approximation. However, this also leads to deeper circuits which, in practice, leads to more error accumulation (a non-negligible concern on near-term quantum devices).
Today, we will study the time evolution of the Ising model on linear lattices of and sites. These lattices consist of an array of spins that interact only with their nearest neighbors. These spins can have two orientations: and , which correspond to a magnetization of and respectively.
where describes the interaction energy, and the magnitude of an external field (in the x-direction above, but we will modify this). Let us write this expression using Pauli matrices, and considering that the external field has an angle with respect to the transversal direction,
This Hamiltonian is useful in that it allows us to easily study the effects of an external field. In the computational basis, the system will be encoded as follows:
| Quantum state | Spin representation |
|---|---|
We will start investigating the time evolution of such a quantum system. More specifically, we will visualize the time-evolution of certain properties of the system like magnetization.
1.1 Requirements
Before starting this tutorial, be sure you have the following installed:
- Qiskit SDK v1.2 or later (
pip install qiskit) - Qiskit Runtime v0.30 or later (
pip install qiskit-ibm-runtime) - Numpy v1.24.1 or later < 2 (
pip install numpy)
1.2 Import the libraries
Note that some libraries that might be useful (MatrixExponential, QDrift) are included even though they are not used in this current notebook. You can try them out if you have time!
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-ibm-runtime
# Check the version of Qiskit
import qiskit
qiskit.__version__
'2.0.2'
# Import the qiskit library
import numpy as np
import matplotlib.pylab as plt
import warnings
from qiskit import QuantumCircuit
from qiskit.circuit.library import PauliEvolutionGate
from qiskit.primitives import StatevectorEstimator
from qiskit.quantum_info import Statevector, SparsePauliOp
from qiskit.synthesis import (
SuzukiTrotter,
LieTrotter,
)
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2
warnings.filterwarnings("ignore")
2. Mapping your problem
2.1 Defining the transverse-field Ising Hamiltonian
We here consider the 1-D transverse-field Ising model.
First, we will create a function that takes in the system parameters , , and , and returns our Hamiltonian as a SparsePauliOp. A SparsePauliOp is a sparse representation of an operator in terms of weighted Pauli terms.
def get_hamiltonian(nqubits, J, h, alpha):
# List of Hamiltonian terms as 3-tuples containing
# (1) the Pauli string,
# (2) the qubit indices corresponding to the Pauli string,
# (3) the coefficient.
ZZ_tuples = [("ZZ", [i, i + 1], -J) for i in range(0, nqubits - 1)]
Z_tuples = [("Z", [i], -h * np.sin(alpha)) for i in range(0, nqubits)]
X_tuples = [("X", [i], -h * np.cos(alpha)) for i in range(0, nqubits)]
# We create the Hamiltonian as a SparsePauliOp, via the method
# `from_sparse_list`, and multiply by the interaction term.
hamiltonian = SparsePauliOp.from_sparse_list(
[*ZZ_tuples, *Z_tuples, *X_tuples], num_qubits=nqubits
)
return hamiltonian.simplify()
Define the Hamiltonian
The system that we now consider has a size of , , and as an example.
n_qubits = 6
hamiltonian = get_hamiltonian(nqubits=n_qubits, J=0.2, h=1.2, alpha=np.pi / 8.0)
hamiltonian
SparsePauliOp(['IIIIZZ', 'IIIZZI', 'IIZZII', 'IZZIII', 'ZZIIII', 'IIIIIZ', 'IIIIZI', 'IIIZII', 'IIZIII', 'IZIIII', 'ZIIIII', 'IIIIIX', 'IIIIXI', 'IIIXII', 'IIXIII', 'IXIIII', 'XIIIII'],
coeffs=[-0.2 +0.j, -0.2 +0.j, -0.2 +0.j, -0.2 +0.j,
-0.2 +0.j, -0.45922012+0.j, -0.45922012+0.j, -0.45922012+0.j,
-0.45922012+0.j, -0.45922012+0.j, -0.45922012+0.j, -1.10865544+0.j,
-1.10865544+0.j, -1.10865544+0.j, -1.10865544+0.j, -1.10865544+0.j,
-1.10865544+0.j])
2.2 Set the parameters of the time-evolution simulation
Here we will consider three different Trotterization techniques:
- Lie–Trotter (first order)
- second-order Suzuki–Trotter
- fourth-order Suzuki–Trotter
The latter two will be used in the exercise, and in the appendix.
num_timesteps = 60
evolution_time = 30.0
dt = evolution_time / num_timesteps
product_formula_lt = LieTrotter()
product_formula_st2 = SuzukiTrotter(order=2)
product_formula_st4 = SuzukiTrotter(order=4)
2.3 Prepare the quantum circuit 1 (Initial state)
Create an initial state. Here we will start with a spin configuration of .
initial_circuit = QuantumCircuit(n_qubits)
initial_circuit.prepare_state("001100")
# Change reps and see the difference when you decompose the circuit
initial_circuit.decompose(reps=1).draw("mpl")
2.4 Prepare the quantum circuit 2 (Single circuit for time evolution)
We here construct a circuit for a single time step using Lie–Trotter.
The Lie product formula (first-order) is implemented in the LieTrotter class. A first-order formula consists of the approximation stated in the introduction, where the matrix exponential of a sum is approximated by a product of matrix exponentials:
As previously mentioned, very deep circuits lead to the accumulation of errors, and cause problems for modern quantum computers. Because two-qubit gates have higher error rates than single-qubit gates, a quantity of particular interest is the two-qubit circuit depth. What really matters is the two-qubit circuit depth after transpilation (since that is the circuit the quantum computer actually executes). But let us get in the habit of counting the operations for this circuit, even now using the simulator.
single_step_evolution_gates_lt = PauliEvolutionGate(
hamiltonian, dt, synthesis=product_formula_lt
)
single_step_evolution_lt = QuantumCircuit(n_qubits)
single_step_evolution_lt.append(
single_step_evolution_gates_lt, single_step_evolution_lt.qubits
)
print(
f"""
Trotter step with Lie-Trotter
-----------------------------
Depth: {single_step_evolution_lt.decompose(reps=3).depth()}
Gate count: {len(single_step_evolution_lt.decompose(reps=3))}
Nonlocal gate count: {single_step_evolution_lt.decompose(reps=3).num_nonlocal_gates()}
Gate breakdown: {", ".join([f"{k.upper()}: {v}" for k, v in single_step_evolution_lt.decompose(reps=3).count_ops().items()])}
"""
)
single_step_evolution_lt.decompose(reps=3).draw("mpl", fold=-1)
Trotter step with Lie-Trotter
-----------------------------
Depth: 17
Gate count: 27
Nonlocal gate count: 10
Gate breakdown: U3: 12, CX: 10, U1: 5
2.5 Set the operators to measure
Let us define a magnetization operator