Approximate quantum compilation for time evolution circuits
Die Seide is noch nich ibersetzt worn. Se guggen de englsche Originalversion.
Usage estimate: Five minutes on an Eagle processor (NOTE: This is an estimate only. Your runtime might vary.)
Background
This tutorial demonstrates how to implement Approximate Quantum Compilation using tensor networks (AQC-Tensor) with Qiskit to enhance quantum circuit performance. We apply AQC-Tensor within the context of a Trotterized time evolution to reduce circuit depth while maintaining simulation accuracy, following the Qiskit framework for state preparation and optimization. You will learn how to create a low-depth ansatz circuit from an initial Trotter circuit, optimize it with tensor networks, and prepare it for quantum hardware execution.
The primary objective is to simulate time evolution for a model Hamiltonian with a reduced circuit depth. This is achieved using the AQC-Tensor Qiskit addon, qiskit-addon-aqc-tensor, which leverages tensor networks, specifically matrix product states (MPS), to compress and optimize the initial circuit. Through iterative adjustments, the compressed ansatz circuit maintains fidelity to the original circuit while staying feasible for near-term quantum hardware. More details can be found in the corresponding docs with a simple example to get started.
Approximate Quantum Compilation is particularly advantageous in quantum simulations that exceed hardware coherence times, as it allows complex simulations to be performed more efficiently. This tutorial guides you through the AQC-Tensor workflow setup in Qiskit, covering initialization of a Hamiltonian, generation of Trotter circuits, and transpilation of the final optimized circuit for a target device.
Requirements
Before starting this tutorial, ensure that you have the following installed:
- Qiskit SDK v1.0 or later, with visualization support
- Qiskit Runtime v0.22 or later (
pip install qiskit-ibm-runtime) - AQC-Tensor Qiskit addon (
pip install 'qiskit-addon-aqc-tensor[aer,quimb-jax]')
Setup
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-addon-aqc-tensor qiskit-addon-utils qiskit-ibm-runtime quimb rustworkx scipy
import numpy as np
import quimb.tensor
import datetime
import matplotlib.pyplot as plt
from scipy.optimize import OptimizeResult, minimize
from qiskit.quantum_info import SparsePauliOp, Pauli
from qiskit.transpiler import CouplingMap
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit import QuantumCircuit
from qiskit.synthesis import SuzukiTrotter
from qiskit_addon_utils.problem_generators import (
generate_time_evolution_circuit,
)
from qiskit_addon_aqc_tensor.ansatz_generation import (
generate_ansatz_from_circuit,
)
from qiskit_addon_aqc_tensor.objective import MaximizeStateFidelity
from qiskit_addon_aqc_tensor.simulation.quimb import QuimbSimulator
from qiskit_addon_aqc_tensor.simulation import tensornetwork_from_circuit
from qiskit_addon_aqc_tensor.simulation import compute_overlap
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import EstimatorV2 as Estimator
from rustworkx.visualization import graphviz_draw
Part I. Small-scale example
The first part of this tutorial uses a small-scale example with 10 sites to illustrate the process of mapping a quantum simulation problem to an executable quantum circuit. Here, we’ll explore the dynamics of a 10-site XXZ model, allowing us to build and optimize a manageable quantum circuit before scaling to larger systems.
The XXZ model is widely studied in physics for examining spin interactions and magnetic properties. We set up the Hamiltonian to have open boundary conditions with site-dependent interactions between neighboring sites along the chain.
Model Hamiltonian and observable
The Hamiltonian for our 10-site XXZ model is defined as:
where is a random coefficient corresponding to edge , and is the number of sites.
By simulating the evolution of this system with reduced circuit depth, we can gain insights into using AQC-Tensor to compress and optimize circuits.
Set up the Hamiltonian and observable
Before we map our problem, we need to set up the coupling map, Hamiltonian, and observable for the 10-site XXZ model.
# L is the number of sites, also the length of the 1D spin chain
L = 10
# Generate the coupling map
edge_list = [(i - 1, i) for i in range(1, L)]
# Generate an edge-coloring so we can make hw-efficient circuits
even_edges = edge_list[::2]
odd_edges = edge_list[1::2]
coupling_map = CouplingMap(edge_list)
# Generate random coefficients for our XXZ Hamiltonian
np.random.seed(0)
Js = np.random.rand(L - 1) + 0.5 * np.ones(L - 1)
hamiltonian = SparsePauliOp(Pauli("I" * L))
for i, edge in enumerate(even_edges + odd_edges):
hamiltonian += SparsePauliOp.from_sparse_list(
[
("XX", (edge), Js[i] / 2),
("YY", (edge), Js[i] / 2),
("ZZ", (edge), Js[i]),
],
num_qubits=L,
)
# Generate a ZZ observable between the two middle qubits
observable = SparsePauliOp.from_sparse_list(
[("ZZ", (L // 2 - 1, L // 2), 1.0)], num_qubits=L
)
print("Hamiltonian:", hamiltonian)
print("Observable:", observable)
graphviz_draw(coupling_map.graph, method="circo")
Hamiltonian: SparsePauliOp(['IIIIIIIIII', 'IIIIIIIIXX', 'IIIIIIIIYY', 'IIIIIIIIZZ', 'IIIIIIXXII', 'IIIIIIYYII', 'IIIIIIZZII', 'IIIIXXIIII', 'IIIIYYIIII', 'IIIIZZIIII', 'IIXXIIIIII', 'IIYYIIIIII', 'IIZZIIIIII', 'XXIIIIIIII', 'YYIIIIIIII', 'ZZIIIIIIII', 'IIIIIIIXXI', 'IIIIIIIYYI', 'IIIIIIIZZI', 'IIIIIXXIII', 'IIIIIYYIII', 'IIIIIZZIII', 'IIIXXIIIII', 'IIIYYIIIII', 'IIIZZIIIII', 'IXXIIIIIII', 'IYYIIIIIII', 'IZZIIIIIII'],
coeffs=[1. +0.j, 0.52440675+0.j, 0.52440675+0.j, 1.0488135 +0.j,
0.60759468+0.j, 0.60759468+0.j, 1.21518937+0.j, 0.55138169+0.j,
0.55138169+0.j, 1.10276338+0.j, 0.52244159+0.j, 0.52244159+0.j,
1.04488318+0.j, 0.4618274 +0.j, 0.4618274 +0.j, 0.9236548 +0.j,
0.57294706+0.j, 0.57294706+0.j, 1.14589411+0.j, 0.46879361+0.j,
0.46879361+0.j, 0.93758721+0.j, 0.6958865 +0.j, 0.6958865 +0.j,
1.391773 +0.j, 0.73183138+0.j, 0.73183138+0.j, 1.46366276+0.j])
Observable: SparsePauliOp(['IIIIZZIIII'],
coeffs=[1.+0.j])
With the Hamiltonian defined, we can proceed to construct the initial state.
# Generate an initial state
initial_state = QuantumCircuit(L)
for i in range(L):
if i % 2:
initial_state.x(i)
Step 1: Map classical inputs to a quantum problem
Now that we have constructed the Hamiltonian, defining the spin-spin interactions and external magnetic fields that characterize the system, we follow three main steps in the AQC-Tensor workflow:
- Generate the optimized AQC circuit: Using Trotterization, we approximate the initial evolution, which is then compressed to reduce circuit depth.
- Create the remaining time evolution circuit: Capture the evolution for the remaining time beyond the initial segment.
- Combine the circuits: Merge the optimized AQC circuit with the remaining evolution circuit into a complete time-evolution circuit ready for execution.
This approach creates a low-depth ansatz for the target evolution, supporting efficient simulation within near-term quantum hardware constraints.
Determine the portion of time evolution to simulate classically
Our goal is to simulate the time evolution of the model Hamiltonian defined earlier using Trotter evolution. To make this process efficient for quantum hardware, we split the evolution into two segments:
-
Initial Segment: This initial portion of the evolution, from to , is simulable with MPS and can be efficiently “compiled” using AQC-Tensor. By using the AQC-Tensor Qiskit addon, we generate a compressed circuit for this segment, referred to as the
aqc_target_circuit. Because this segment will be simulated on a tensor-network simulator, we can afford to use a higher number of Trotter layers without impacting hardware resources significantly. We setaqc_target_num_trotter_steps = 32for this segment. -
Subsequent Segment: This remaining portion of the evolution, from to , will be executed on quantum hardware, referred to as the
subsequent_circuit. Given hardware limitations, we aim to use as few Trotter layers as possible to maintain a manageable circuit depth. For this segment, we usesubsequent_num_trotter_steps = 3.
Choose the split time
We choose as the split time to balance classical simulability with hardware feasibility. Early in the evolution, entanglement in the XXZ model remains low enough for classical methods like MPS to approximate accurately.
When choosing a split time, a good guideline is to select a point where entanglement is still manageable classically but captures enough of the evolution to simplify the hardware-executed portion. Trial and error may be needed to find the best balance for different Hamiltonians.
# Generate the AQC target circuit (initial segment)
aqc_evolution_time = 0.2
aqc_target_num_trotter_steps = 32
aqc_target_circuit = initial_state.copy()
aqc_target_circuit.compose(
generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(reps=aqc_target_num_trotter_steps),
time=aqc_evolution_time,
),
inplace=True,
)
# Generate the subsequent circuit
subsequent_num_trotter_steps = 3
subsequent_evolution_time = 0.2
subsequent_circuit = generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(reps=subsequent_num_trotter_steps),
time=subsequent_evolution_time,
)
subsequent_circuit.draw("mpl", fold=-1)

To enable a meaningful comparison, we will generate two additional circuits:
-
AQC comparison circuit: This circuit evolves up to
aqc_evolution_timebut uses the same Trotter step duration as thesubsequent_circuit. It serves as a comparison to theaqc_target_circuit, showing the evolution we would observe without using an increased number of Trotter steps. We will refer to this circuit as theaqc_comparison_circuit. -
Reference circuit: This circuit is used as a baseline to obtain the exact result. It simulates the full evolution using tensor networks to calculate the exact outcome, providing a reference for evaluating the effectiveness of AQC-Tensor. We will refer to this circuit as the
reference_circuit.
# Generate the AQC comparison circuit
aqc_comparison_num_trotter_steps = int(
subsequent_num_trotter_steps
/ subsequent_evolution_time
* aqc_evolution_time
)
print(
"Number of Trotter steps for comparison:",
aqc_comparison_num_trotter_steps,
)
aqc_comparison_circuit = generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(reps=aqc_comparison_num_trotter_steps),
time=aqc_evolution_time,
)
Number of Trotter steps for comparison: 3
# Generate the reference circuit
evolution_time = 0.4
reps = 200
reference_circuit = initial_state.copy()
reference_circuit.compose(
generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(reps=reps),
time=evolution_time,
),
inplace=True,
)
Generate an ansatz and initial parameters from a Trotter circuit with fewer steps
Now that we have constructed our four circuits, let's proceed with the AQC-Tensor workflow. First, we construct a “good” circuit that has the same evolution time as the target circuit, but with fewer Trotter steps (and thus fewer layers).
Then we pass this “good” circuit to AQC-Tensor’s generate_ansatz_from_circuit function. This function analyzes the two-qubit connectivity of the circuit and returns two things:
- A general, parametrized ansatz circuit with the same two-qubit connectivity as the input circuit.
- Parameters that, when plugged into the ansatz, yield the input (good) circuit.
Soon we will take these parameters and iteratively adjust them to bring the ansatz circuit as close as possible to the target MPS.
aqc_ansatz_num_trotter_steps = 1
aqc_good_circuit = initial_state.copy()
aqc_good_circuit.compose(
generate_time_evolution_circuit(
hamiltonian,
synthesis=SuzukiTrotter(reps=aqc_ansatz_num_trotter_steps),
time=aqc_evolution_time,
),
inplace=True,
)
aqc_ansatz, aqc_initial_parameters = generate_ansatz_from_circuit(
aqc_good_circuit
)
aqc_ansatz.draw("mpl", fold=-1)

print(f"AQC Comparison circuit: depth {aqc_comparison_circuit.depth()}")
print(f"Target circuit: depth {aqc_target_circuit.depth()}")
print(
f"Ansatz circuit: depth {aqc_ansatz.depth()}, with {len(aqc_initial_parameters)} parameters"
)
AQC Comparison circuit: depth 36
Target circuit: depth 385
Ansatz circuit: depth 7, with 156 parameters
Choose settings for tensor network simulation
Here, we use Quimb's matrix-product state circuit simulator, along with jax to provide the gradient.
simulator_settings = QuimbSimulator(
quimb.tensor.CircuitMPS, autodiff_backend="jax"
)
Next, we build a MPS representation of the target state that will be approximated using AQC-Tensor. This representation enables efficient handling of entanglement, providing a compact description of the quantum state for further optimization.
aqc_target_mps = tensornetwork_from_circuit(
aqc_target_circuit, simulator_settings
)
print("Target MPS maximum bond dimension:", aqc_target_mps.psi.max_bond())
# Obtains the reference MPS, where we can obtain the exact expectation value by examining the `local_expectation``
reference_mps = tensornetwork_from_circuit(
reference_circuit, simulator_settings
)
reference_expval = reference_mps.local_expectation(
quimb.pauli("Z") & quimb.pauli("Z"), (L // 2 - 1, L // 2)
).real.item()
print("Reference MPS maximum bond dimension:", reference_mps.psi.max_bond())
Target MPS maximum bond dimension: 5
Reference MPS maximum bond dimension: 7
Note that, by choosing a larger number of Trotter steps for the target state, we have effectively reduced its Trotter error compared to the initial circuit. We can evaluate the fidelity (