Quickstart
In this guide we will use the pauli-prop package to classically simulate the time dynamics of a 10-qubit kicked Ising model on a 1D spin chain.
1. Prepare the inputs for Pauli propagation
The Hamiltonian considered is:
where describes the coupling of nearest-neighbor spins, , and is the global transverse field. A first-order Trotter decomposition of the time-evolved operator will be implemented as a quantum circuit, , over Trotter steps. The coupling constant, , will be fixed at , and will be fixed at . The interactions will be implemented using Clifford gates (, , ).
We implement the Trotterized time evolution as a quantum circuit and use for the non-Clifford rotations about the x-axis. The further these angles are from Clifford angles (i.e. ), the more difficult the system will be to simulate for Pauli propagation methods.
For the choice of observable, we will consider the average single-site magnetization, , where is the number of spins.
import numpy as np
from qiskit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler import CouplingMap
num_qubits = 10
coupling_map = CouplingMap.from_line(num_qubits, bidirectional=False)
# Num Trotter steps
num_steps = 20
theta_rx = np.pi / 6
# Average single-site magnetization
observable = (
SparsePauliOp(
[
"I" * iq + "Z" + "I" * (num_qubits - iq - 1)
for iq in range(num_qubits)
]
)
/ num_qubits
)
# Create the Trotter circuit
num_qubits = 10
num_steps = 20
theta_rx = np.pi / 6
circuit = QuantumCircuit(num_qubits)
edges = CouplingMap.from_line(num_qubits, bidirectional=False).get_edges()
for _ in range(num_steps):
circuit.rx(theta_rx, [i for i in range(num_qubits)])
for edge in edges:
circuit.sdg(edge)
circuit.ry(np.pi / 2, edge[1])
circuit.cx(edge[0], edge[1])
circuit.ry(-np.pi / 2, edge[1])
circuit.draw("mpl", fold=-1)Output:
Simulate the time evolution of the system with Pauli propagation
Once we have our circuit, , and observable, , we can easily simulate the system in a few steps:
- Separate , into its Clifford, , and non-Clifford, , parts such that using
evolve_through_cliffords - Propagate through , resulting in a new operator, , using
pauli_prop.propagate_through_circuit - Evolve through the Clifford part of the circuit using Qiskit's built-in Clifford evolution support
- Approximate the expectation value as by summing the coefficients in associated with fully-diagonal Pauli terms (i.e. Pauli terms containing either
IorZon all qubits). Remember, this is an approximation because we truncated terms from as we propagated it through the non-Clifford part of the circuit.
import time
from pauli_prop import evolve_through_cliffords, propagate_through_circuit
cliff, non_cliff = evolve_through_cliffords(circuit)
max_terms_list = [10**i for i in range(8)]
approx_evs = []
durations = []
for max_terms in max_terms_list:
st = time.perf_counter()
evolved_obs = propagate_through_circuit(
observable, non_cliff, max_terms=max_terms, atol=1e-12, frame="h"
)[0]
evolved_obs.paulis = evolved_obs.paulis.evolve(cliff, frame="h")
durations.append(time.perf_counter() - st)
approx_evs.append(
float(evolved_obs.coeffs[~evolved_obs.paulis.x.any(axis=1)].sum())
)As we run larger calculations, the expectation value approximations become more accurate. In this example, we saturate the full Pauli space at around , which is reflected in the curve flattening out between the final two points.
While the plot below shows a monotonic convergence, Pauli propagation simulations do not generally converge monotonically. It is not unusual to see "bumpy" behavior in these types of plots.
import matplotlib.pyplot as plt
from qiskit_aer import AerSimulator
sim_circ = circuit.copy()
sim_circ.save_statevector()
backend = AerSimulator(method="statevector")
psi = backend.run(sim_circ).result().data()["statevector"]
exact_ev = psi.expectation_value(observable)
ax1 = plt.gca()
ax1.plot(max_terms_list, approx_evs, marker="o", label="Approximate")
ax1.axhline(exact_ev, linestyle="--", color="green", label="Exact")
ax1.set_xscale("log")
ax1.set_xlabel("# terms kept")
ax1.set_ylabel(r"$\frac{1}{N} \sum_{i=1}^{N} \langle z_i \rangle$")
ax2 = ax1.twinx()
ax2.plot(
max_terms_list, durations, marker=".", label="Runtime", color="orange"
)
ax2.set_ylabel("Runtime (s)", color="orange")
ax2.set_yscale("log")
handles1, labels1 = ax1.get_legend_handles_labels()
handles2, labels2 = ax2.get_legend_handles_labels()
ax1.legend(handles1 + handles2, labels1 + labels2, loc="lower right")
plt.title(f"Simulating {num_steps}-step 1D Ising Model")Output:
Text(0.5, 1.0, 'Simulating 20-step 1D Ising Model')