Benchmark dynamic circuits with cut Bell pairs
Halaman ini belum diterjemahkan. Anda sedang melihat versi asli dalam bahasa Inggris.
Usage estimate: 22 seconds on a Heron r2 processor (NOTE: This is an estimate only. Your runtime may vary.)
Background
Quantum hardware is typically limited to local interactions, but many algorithms require entangling distant qubits or even qubits on separate processors. Dynamic circuits - that is, circuits with mid-circuit measurement and feedforward - provide a way to overcome these limitations by using real-time classical communication to effectively implement non-local quantum operations. In this approach, measurement outcomes from one part of a circuit (or one QPU) can conditionally trigger gates on another, allowing us to teleport entanglement across long distances. This forms the basis of local operations and classical communication (LOCC) schemes, where we consume entangled resource states (Bell pairs) and communicate measurement results classically to link distant qubits.
One promising use of LOCC is to realize virtual long-range CNOT gates by teleportation, as shown in the long-range entanglement tutorial. Instead of a direct long-range CNOT (which hardware connectivity might not permit), we create Bell pairs and perform a teleportation-based gate implementation. However, the fidelity of such operations depends on hardware characteristics. Qubit decoherence during the necessary delay (while waiting for measurement results) and classical communication latency can degrade the entangled state. Also, errors on mid-circuit measurements are harder to correct than errors on final measurements as they propagate to the rest of the circuit through the conditional gates.
In the reference experiment, the authors introduce a Bell pair fidelity benchmark to identify which parts of a device are best suited for LOCC-based entanglement. The idea is to run a small dynamic circuit on every group of four connected qubits in the processor. This four-qubit circuit first creates a Bell pair on two middle qubits, then uses those as a resource to entangle the two edge qubits by using LOCC. Concretely, qubits 1 and 2 are prepared into an uncut Bell pair locally (using a Hadamard and CNOT), and then a teleportation routine consumes that Bell pair to entangle qubits 0 and 3. Qubits 1 and 2 are measured during execution of the circuit, and based on those outcomes, Pauli corrections (an X on qubit 3 and Z on qubit 0) are applied. Qubits 0 and 3 are then left in a Bell state at the end of the circuit.
To quantify the quality of this final entangled pair, we measure its stabilizers: specifically, the parity in the basis () and in the basis (). For a perfect Bell pair, both of these expectations equal +1. In practice, hardware noise will reduce these values. We therefore repeat the circuit twice for each qubit-pair: one circuit measures qubits 0 and 3 in the basis, and another measures them in the basis. From the results, we obtain an estimate of and for that pair of qubits. We use the mean squared error (MSE) of these stabilizers with respect to the ideal value (1) as a simple metric of entanglement fidelity. A lower MSE means the two qubits achieved a Bell state closer to ideal (higher fidelity), whereas a higher MSE indicates more error. By scanning this experiment across the device, we can benchmark the measurement-and-feedforward capability of different qubit groups and identify the best pairs of qubits for LOCC operations.
This tutorial demonstrates the experiment on an IBM Quantum® device to illustrate how dynamic circuits can be used to generate and evaluate entanglement between distant qubits. We will map out all four-qubit linear chains on the device, run the teleportation circuit on each, and then visualize the distribution of MSE values. This end-to-end procedure shows how to leverage Qiskit Runtime and dynamic circuit features to inform hardware-aware choices for cutting circuits or distributing quantum algorithms across a modular system.
Requirements
Before starting this tutorial, ensure that you have the following installed:
- Qiskit SDK v2.0 or later, with visualization support
- Qiskit Runtime v0.40 or later (
pip install qiskit-ibm-runtime)
Setup
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-ibm-runtime
from qiskit import QuantumCircuit
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler
from qiskit.transpiler import generate_preset_pass_manager
import numpy as np
import matplotlib.pyplot as plt
def create_bell_stab(initial_layouts):
"""
Create a circuit for a 1D chain of qubits (number of qubits must be a multiple of 4),
where a middle Bell pair is consumed to create a Bell at the edge.
Takes as input a list of lists, where each element of the list is a
1D chain of physical qubits that is used as the initial_layout for the transpiled circuit.
Returns a list of length-2 tuples, each tuple contains a circuit to measure the ZZ stabilizer and
a circuit to measure the XX stabilizer of the edge Bell state.
"""
bell_circuits = []
for (
initial_layout
) in initial_layouts: # Iterate over chains of physical qubits
assert (
len(initial_layout) % 4 == 0
), f"The length of the chain must be a multiple of 4, len(inital_layout)={len(initial_layout)}"
num_pairs = len(initial_layout) // 4
bell_parallel = QuantumCircuit(4 * num_pairs, 4 * num_pairs)
for pair_idx in range(num_pairs):
(q0, q1, q2, q3) = (
pair_idx * 4,
pair_idx * 4 + 1,
pair_idx * 4 + 2,
pair_idx * 4 + 3,
)
(c0, c1) = pair_idx * 4, pair_idx * 4 + 3 # edge qubits
(ca0, ca1) = pair_idx * 4 + 1, pair_idx * 4 + 2 # middle qubits
bell_parallel.h(q0)
bell_parallel.h(q1)
bell_parallel.cx(q1, q2)
bell_parallel.cx(q0, q1)
bell_parallel.cx(q2, q3)
bell_parallel.h(q2)
# add barrier BEFORE measurements and add id in conditional
bell_parallel.barrier()
for pair_idx in range(num_pairs):
(q0, q1, q2, q3) = (
pair_idx * 4,
pair_idx * 4 + 1,
pair_idx * 4 + 2,
pair_idx * 4 + 3,
)
(ca0, ca1) = pair_idx * 4 + 1, pair_idx * 4 + 2 # middle qubits
bell_parallel.measure(q1, ca0)
bell_parallel.measure(q2, ca1)
# bell_parallel.barrier() #remove barrier after measurement
for pair_idx in range(num_pairs):
(q0, q1, q2, q3) = (
pair_idx * 4,
pair_idx * 4 + 1,
pair_idx * 4 + 2,
pair_idx * 4 + 3,
)
(ca0, ca1) = pair_idx * 4 + 1, pair_idx * 4 + 2 # middle qubits
with bell_parallel.if_test((ca0, 1)):
bell_parallel.x(q3)
with bell_parallel.if_test((ca1, 1)):
bell_parallel.z(q0)
bell_parallel.id(q0) # add id here for correct alignment
bell_zz = bell_parallel.copy()
bell_zz.barrier()
bell_xx = bell_parallel.copy()
bell_xx.barrier()
for pair_idx in range(num_pairs):
(q0, q1, q2, q3) = (
pair_idx * 4,
pair_idx * 4 + 1,
pair_idx * 4 + 2,
pair_idx * 4 + 3,
)
bell_xx.h(q0)
bell_xx.h(q3)
bell_xx.barrier()
for pair_idx in range(num_pairs):
(q0, q1, q2, q3) = (
pair_idx * 4,
pair_idx * 4 + 1,
pair_idx * 4 + 2,
pair_idx * 4 + 3,
)
(c0, c1) = pair_idx * 4, pair_idx * 4 + 3 # edge qubits
bell_zz.measure(q0, c0)
bell_zz.measure(q3, c1)
bell_xx.measure(q0, c0)
bell_xx.measure(q3, c1)
bell_circuits.append(bell_zz)
bell_circuits.append(bell_xx)
return bell_circuits
def get_mse(result, initial_layouts):
"""
given a result object and the initial layouts, returns a dict of layouts and their mse
"""
layout_mse = {}
for layout_idx, initial_layout in enumerate(initial_layouts):
layout_mse[tuple(initial_layout)] = {}
num_pairs = len(initial_layout) // 4
counts_zz = result[2 * layout_idx].data.c.get_counts()
total_shots = sum(counts_zz.values())
# Get ZZ expectation value
exp_zz_list = []
for pair_idx in range(num_pairs):
exp_zz = 0
for bitstr, shots in counts_zz.items():
bitstr = bitstr[::-1] # reverse order to big endian
b1, b0 = (
bitstr[pair_idx * 4],
bitstr[pair_idx * 4 + 3],
) # parse bitstring to get edge measurements for each 4-q chain
z_val0 = 1 if b0 == "0" else -1
z_val1 = 1 if b1 == "0" else -1
exp_zz += z_val0 * z_val1 * shots
exp_zz /= total_shots
exp_zz_list.append(exp_zz)
counts_xx = result[2 * layout_idx + 1].data.c.get_counts()
total_shots = sum(counts_xx.values())
# Get XX expectation value
exp_xx_list = []
for pair_idx in range(num_pairs):
exp_xx = 0
for bitstr, shots in counts_xx.items():
bitstr = bitstr[::-1] # reverse order to big endian
b1, b0 = (
bitstr[pair_idx * 4],
bitstr[pair_idx * 4 + 3],
) # parse bitstring to get edge measurements for each 4-q chain
x_val0 = 1 if b0 == "0" else -1
x_val1 = 1 if b1 == "0" else -1
exp_xx += x_val0 * x_val1 * shots
exp_xx /= total_shots
exp_xx_list.append(exp_xx)
mse_list = [
((exp_zz - 1) ** 2 + (exp_xx - 1) ** 2) / 2
for exp_zz, exp_xx in zip(exp_zz_list, exp_xx_list)
]
print(f"layout {initial_layout}")
for idx in range(num_pairs):
layout_mse[tuple(initial_layout)][
tuple(initial_layout[4 * idx : 4 * idx + 4])
] = mse_list[idx]
print(
f"qubits: {initial_layout[4*idx:4*idx+4]}, mse:, {round(mse_list[idx],4)}"
)
# print(f'exp_zz: {round(exp_zz_list[idx],4)}, exp_xx: {round(exp_xx_list[idx],4)}')
print(" ")
return layout_mse
def plot_mse_ecdfs(layouts_mse, combine_layouts=False):
"""
Plot CDF of MSE data for multiple layouts. Optionally combine all data in a single CDF
"""
if not combine_layouts:
for initial_layout, layouts in layouts_mse.items():
sorted_layouts = dict(
sorted(layouts.items(), key=lambda item: item[1])
) # sort layouts by mse
# get layouts and mses
layout_list = list(sorted_layouts.keys())
mse_list = np.asarray(list(sorted_layouts.values()))
# convert to numpy
x = np.array(mse_list)
y = np.arange(1, len(x) + 1) / len(x)
# Prepend (x[0], 0) to start CDF at zero
x = np.insert(x, 0, x[0])
y = np.insert(y, 0, 0)
# Create the plot
plt.plot(
x,
y,
marker="x",
linestyle="-",
label=f"qubits: {initial_layout}",
)
# add qubits labels for the edge pairs
for xi, yi, q in zip(x[1:], y[1:], layout_list):
plt.annotate(
[q[0], q[3]],
(xi, yi),
textcoords="offset points",
xytext=(5, -10),
ha="left",
fontsize=8,
)
elif combine_layouts:
all_layouts = {}
all_initial_layout = []
for (
initial_layout,
layouts,
) in layouts_mse.items(): # puts together all layout information
all_layouts.update(layouts)
all_initial_layout += initial_layout
sorted_layouts = dict(
sorted(all_layouts.items(), key=lambda item: item[1])
) # sort layouts by mse
# get layouts and mses
layout_list = list(sorted_layouts.keys())
mse_list = np.asarray(list(sorted_layouts.values()))
# convert to numpy
x = np.array(mse_list)
y = np.arange(1, len(x) + 1) / len(x)
# Prepend (x[0], 0) to start CDF at zero
x = np.insert(x, 0, x[0])
y = np.insert(y, 0, 0)
# Create the plot
plt.plot(
x,
y,
marker="x",
linestyle="-",
label=f"qubits: {sorted(list(set(all_initial_layout)))}",
)
# add qubit labels for the edge pairs
for xi, yi, q in zip(x[1:], y[1:], layout_list):
plt.annotate(
[q[0], q[3]],
(xi, yi),
textcoords="offset points",
xytext=(5, -10),
ha="left",
fontsize=8,
)
plt.xscale("log")
plt.xlabel("Mean squared error of ⟨ZZ⟩ and ⟨XX⟩")
plt.ylabel("Cumulative distribution function")
plt.title("CDF for different initial layouts")
plt.grid(alpha=0.3)
plt.show()
Step 1: Map classical inputs to a quantum problem
The first step is to create a set of quantum circuits to benchmark all candidate Bell-pair links tailored to the device's topology. We programmatically search the device coupling map for all linearly-connected chains of four qubits. Each such chain (labeled by qubit indices ) serves as a test case for the entanglement-swapping circuit. By identifying all possible length-4 paths, we ensure maximum coverage for possible grouping of qubits that could realize the protocol.
service = QiskitRuntimeService()
backend = service.least_busy(operational=True)
We generate these chains by using a helper function that performs a greedy search on the device graph. It returns "stripes" of four four-qubit chains bundled into 16-qubit groups (dynamic circuits currently constrain the size of the measurement register to 16 qubits). Bundling allows us to run multiple four-qubit experiments in parallel on distinct parts of the chip, and make efficient use of the whole device. Each 16-qubit stripe contains four disjoint chains, meaning that no qubit is reused within that group. For example, one stripe might consist of chains