Zum Hauptinhalt springen

Weitreichende Verschränkung mit dynamischen Circuits

Geschätzter Verbrauch: 4 Minuten auf einem Heron-r2-Prozessor. (HINWEIS: Dies ist nur eine Schätzung. Die tatsächliche Laufzeit kann abweichen.)

Hintergrund

Weitreichende Verschränkung zwischen entfernten Qubits ist auf Geräten mit eingeschränkter Konnektivität schwierig. Dieses Tutorial zeigt, wie dynamische Circuits eine solche Verschränkung erzeugen können, indem ein weitreichendes Controlled-X-Gate (LRCX) mithilfe eines messungsbasierten Protokolls implementiert wird.

Entsprechend dem Ansatz von Elisa Bäumer et al. in 1 werden Mid-Circuit-Messung und Feedforward genutzt, um Gates konstanter Tiefe unabhängig vom Qubit-Abstand zu erreichen. Dabei werden intermediäre Bell-Paare erzeugt, ein Qubit aus jedem Paar gemessen und klassisch bedingte Gates angewendet, um die Verschränkung über das Gerät hinweg zu propagieren. Dies vermeidet lange SWAP-Ketten und reduziert sowohl die Circuit-Tiefe als auch die Anfälligkeit für Zwei-Qubit-Gate-Fehler.

In diesem Notebook passen wir das Protokoll für IBM Quantum®-Hardware an und erweitern es, um mehrere LRCX-Operationen parallel auszuführen. So können wir untersuchen, wie die Performance mit der Anzahl simultaner bedingter Operationen skaliert.

Voraussetzungen

Stelle vor Beginn dieses Tutorials sicher, dass Folgendes installiert ist:

  • Qiskit SDK v2.0 oder neuer, mit Unterstützung für Visualisierung
  • Qiskit Runtime ( pip install qiskit-ibm-runtime ) v0.37 oder neuer

Einrichtung

# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-ibm-runtime
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit.circuit.classical import expr
from qiskit.transpiler import generate_preset_pass_manager
from qiskit.visualization import plot_circuit_layout
from qiskit_ibm_runtime import (
QiskitRuntimeService,
Batch,
SamplerV2 as Sampler,
)
import matplotlib.pyplot as plt
import numpy as np

Schritt 1: Klassische Eingaben auf ein Quantenproblem abbilden

Wir implementieren jetzt ein weitreichendes CNOT-Gate zwischen zwei entfernten Qubits gemäß der unten gezeigten Dynamic-Circuit-Konstruktion (adaptiert aus Abb. 1a in Ref. 1). Die Kernidee ist, eine „Bus"-Kette aus Ancilla-Qubits, die mit 0|0\rangle initialisiert sind, zu verwenden, um weitreichende Gate-Teleportation zu vermitteln.

Weitreichendes CNOT-Circuit

Wie in der Abbildung dargestellt, funktioniert der Prozess wie folgt:

  1. Eine Kette von Bell-Paaren wird erzeugt, die das Kontroll- und das Ziel-Qubit über intermediäre Ancillas verbindet.
  2. Bell-Messungen werden zwischen nicht verschränkten benachbarten Qubits durchgeführt, wodurch die Verschränkung schrittweise weitergegeben wird, bis das Kontroll- und das Ziel-Qubit ein Bell-Paar teilen.
  3. Dieses Bell-Paar wird für Gate-Teleportation genutzt und verwandelt ein lokales CNOT in ein deterministisches weitreichendes CNOT mit konstanter Tiefe.

Dieser Ansatz ersetzt lange SWAP-Ketten durch ein Protokoll mit konstanter Tiefe, reduziert die Anfälligkeit für Zwei-Qubit-Gate-Fehler und macht die Operation skalierbar mit der Gerätegröße.

Im Folgenden gehen wir zunächst die Dynamic-Circuit-Implementierung des LRCX-Circuits durch. Am Ende stellen wir auch eine unitäre Implementierung zum Vergleich bereit, um die Vorteile dynamischer Circuits in diesem Kontext hervorzuheben.

(i) Circuit initialisieren

Wir beginnen mit einem einfachen Quantenproblem, das als Vergleichsbasis dient. Konkret initialisieren wir einen Circuit mit einem Kontroll-Qubit an Index 0 und wenden ein Hadamard-Gate darauf an. Dies erzeugt einen Superpositionszustand, der nach einer Controlled-X-Operation den Bell-Zustand (00+11)/2(|00\rangle + |11\rangle)/\sqrt{2} zwischen Kontroll- und Ziel-Qubit erzeugt.

In diesem Stadium konstruieren wir noch nicht das weitreichende Controlled-X-Gate (LRCX) selbst. Unser Ziel ist es, einen klaren und minimalen Ausgangs-Circuit zu definieren, der die Rolle des LRCX hervorhebt. In Schritt 2 zeigen wir, wie das LRCX als Optimierung mithilfe dynamischer Circuits implementiert werden kann, und vergleichen seine Performance mit einem unitären Äquivalent. Das LRCX-Protokoll kann auf jeden beliebigen Ausgangs-Circuit angewendet werden. Wir verwenden hier dieses einfache Hadamard-Setup zur Veranschaulichung.

distance = 6  # The distance of the CNOT gate, with the convention that a distance of zero is a nearest-neighbor CNOT.

def initialize_circuit(distance):
assert distance >= 0
control = 0 # control qubit
n = distance # number of qubits between target and control

qr = QuantumRegister(
n + 2, name="q"
) # Circuit with n qubits between control and target
cr = ClassicalRegister(
2, name="cr"
) # Classical register for measuring control and target qubits

k = int(n / 2) # Number of Bell States to be used

allcr = [cr]
if (
distance > 1
): # This classical register will be used to store ZZ measurements. It is only used for long-range CX gates with distance > 1
c1 = ClassicalRegister(
k, name="c1"
) # Classical register needed for post processing
allcr.append(c1)
if (
distance > 0
): # This classical register will be used to store XX measurements. It is only used if distance > 0
c2 = ClassicalRegister(
n - k, name="c2"
) # Classical register needed for post processing
allcr.append(c2)

qc = QuantumCircuit(qr, *allcr, name="CNOT")

# Apply a Hadamard gate to the control qubit such that the long-range CNOT gate will prepare a Bell state (|00> + |11>)/sqrt(2)
qc.h(control)

return qc

qc = initialize_circuit(distance)
qc.draw(fold=-1, output="mpl", scale=0.5)

Ausgabe der vorherigen Code-Zelle

Schritt 2: Problem für die Ausführung auf Quantenhardware optimieren

In diesem Schritt zeigen wir, wie man den LRCX-Circuit mithilfe dynamischer Circuits aufbaut. Das Ziel ist es, den Circuit für die Ausführung auf Hardware zu optimieren, indem die Tiefe im Vergleich zu einer rein unitären Implementierung reduziert wird. Um die Vorteile zu veranschaulichen, zeigen wir sowohl die dynamische LRCX-Konstruktion als auch ihr unitäres Äquivalent und vergleichen später ihre Performance nach der Transpilierung. Das LRCX-Protokoll kann auf jeden Circuit angewendet werden, bei dem ein weitreichendes CNOT benötigt wird — hier verwenden wir das einfache Hadamard-initialisierte Problem zur Illustration.

(ii) Bell-Paare vorbereiten

Wir beginnen mit der Erstellung einer Kette von Bell-Paaren entlang des Pfads zwischen Kontroll- und Ziel-Qubit. Wenn der Abstand ungerade ist, wenden wir zunächst ein CNOT vom Kontroll-Qubit zu seinem Nachbarn an — das ist das CNOT, das teleportiert werden soll. Bei geradem Abstand wird dieses CNOT nach der Bell-Paar-Vorbereitungsphase angewendet. Die Bell-Paar-Kette verschränkt dann aufeinanderfolgende Qubit-Paare und legt so die Ressource bereit, um die Kontrollinformation über das Gerät zu übertragen.

# Determine where to start the Bell pair chain and add an extra CNOT when n is odd
def check_even(n: int) -> int:
"""Return 1 if n is even, else 2."""
return 1 if n % 2 == 0 else 2

def prepare_bell_pairs(qc, add_barriers=True):
n = qc.num_qubits - 2 # number of qubits between target and control
k = int(n / 2)

if add_barriers:
qc.barrier()

x0 = check_even(n)
if n % 2 != 0:
qc.cx(0, 1)

# Create k Bell pairs
for i in range(k):
qc.h(x0 + 2 * i)
qc.cx(x0 + 2 * i, x0 + 2 * i + 1)
return qc

qc = prepare_bell_pairs(qc)
qc.draw(output="mpl", fold=-1, scale=0.5)

Ausgabe der vorherigen Code-Zelle

(iii) Benachbarte Qubit-Paare in der Bell-Basis messen

Als nächstes messen wir nicht verschränkte benachbarte Qubits in der Bell-Basis (Zwei-Qubit-Messungen von XXXX und ZZZZ). Dadurch entsteht ein weitreichendes Bell-Paar zwischen dem Ziel-Qubit und dem Qubit neben dem Kontroll-Qubit (bis auf Pauli-Korrekturen, die im nächsten Schritt per Feedforward implementiert werden). Parallel dazu implementieren wir die verschränkende Messung, die das CNOT-Gate auf das gewünschte Ziel-Qubit teleportiert.

def measure_bell_basis(qc, add_barriers=True):
n = qc.num_qubits - 2 # number of qubits between target and control
k = int(n / 2)

if n > 1:
_, c1, c2 = qc.cregs
elif n > 0:
_, c2 = qc.cregs

# Determine where to start the Bell pair chain and add an extra CNOT when n is odd
x0 = 1 if n % 2 == 0 else 2

# Entangling layer that implements the Bell measurement (and additionally adds the CNOT to be teleported, if n is even)
for i in range(k + 1):
qc.cx(x0 - 1 + 2 * i, x0 + 2 * i)

for i in range(1, k + x0):
if i == 1:
qc.h(2 * i + 1 - x0)
else:
qc.h(2 * i + 1 - x0)

if add_barriers:
qc.barrier()

# Map the ZZ measurements onto classical register c1
for i in range(k):
if i == 0:
qc.measure(2 * i + x0, c1[i])
else:
qc.measure(2 * i + x0, c1[i])

# Map the XX measurements onto classical register c2
for i in range(1, k + x0):
if i == 1:
qc.measure(2 * i + 1 - x0, c2[i - 1])
else:
qc.measure(2 * i + 1 - x0, c2[i - 1])
return qc

qc = measure_bell_basis(qc)
qc.draw(output="mpl", fold=-1, scale=0.5)

Ausgabe der vorherigen Code-Zelle

(iv) Feedforward-Korrekturen anwenden, um Pauli-Nebenprodukte zu korrigieren

Die Bell-Basis-Messungen erzeugen Pauli-Nebenprodukte, die mithilfe der aufgezeichneten Ergebnisse korrigiert werden müssen. Dies geschieht in zwei Schritten. Zunächst muss die Parität aller ZZZZ-Messungen berechnet werden, die dann verwendet wird, um bedingt ein XX-Gate auf das Ziel-Qubit anzuwenden. Ebenso wird die Parität der XXXX-Messungen berechnet und verwendet, um bedingt ein ZZ-Gate auf das Kontroll-Qubit anzuwenden.

Mit dem neuen klassischen Ausdrucks-Framework in Qiskit können diese Paritäten direkt in der klassischen Verarbeitungsschicht des Circuits berechnet werden. Anstatt eine Folge von einzelnen bedingten Gates für jedes Messbit anzuwenden, können wir einen einzigen klassischen Ausdruck erstellen, der das XOR (die Parität) aller relevanten Messergebnisse darstellt. Dieser Ausdruck wird dann als Bedingung in einem einzigen if_test-Block verwendet, sodass die Korrektur-Gates mit konstanter Tiefe angewendet werden können. Dieser Ansatz vereinfacht den Circuit und stellt sicher, dass die Feedforward-Korrekturen keine unnötige zusätzliche Latenz einführen.

def apply_ffwd_corrections(qc):
control = 0 # control qubit
target = qc.num_qubits - 1 # target qubit
n = qc.num_qubits - 2 # number of qubits between target and control

k = int(n / 2)
x0 = check_even(n)

if n > 1:
_, c1, c2 = qc.cregs
elif n > 0:
_, c2 = qc.cregs

# First, let's compute the parity of all ZZ measurements
for i in range(k):
if i == 0:
parity_ZZ = expr.lift(
c1[i]
) # Store the value of the first ZZ measurement in parity_ZZ
else:
parity_ZZ = expr.bit_xor(
c1[i], parity_ZZ
) # Successively compute the parity via XOR operations

for i in range(1, k + x0):
if i == 1:
parity_XX = expr.lift(
c2[i - 1]
) # Store the value of the first XX measurement in parity_XX
else:
parity_XX = expr.bit_xor(
c2[i - 1], parity_XX
) # Successively compute the parity via XOR operations

if n > 0:
with qc.if_test(parity_XX):
qc.z(control)

if n > 1:
with qc.if_test(parity_ZZ):
qc.x(target)
return qc

qc = apply_ffwd_corrections(qc)
qc.draw(output="mpl", fold=-1, scale=0.5)

Ausgabe der vorherigen Code-Zelle

(v) Kontroll- und Ziel-Qubit messen

Wir definieren eine Hilfsfunktion, die die Messung der Kontroll- und Ziel-Qubits in der XXXX-, YYYY- oder ZZZZ-Basis ermöglicht. Zur Verifikation des Bell-Zustands (00+11)/2(|00\rangle + |11\rangle)/\sqrt{2} sollten die Erwartungswerte von XXXX und ZZZZ beide +1+1 betragen, da sie Stabilisatoren des Zustands sind. Die YYYY-Messung wird hier ebenfalls unterstützt und unten bei der Berechnung der Fidelity verwendet.

def measure_in_basis(qc, basis="XX", add_barrier=True):
control = 0 # control qubit
target = qc.num_qubits - 1 # target qubit

assert basis in ["XX", "YY", "ZZ"]

qc = (
qc.copy()
) # We copy the circuit because we want to measure in different bases
cr = qc.cregs[0]

if add_barrier:
qc.barrier()

if basis == "XX":
qc.h(control)
qc.h(target)
elif basis == "YY":
qc.sdg(control)
qc.sdg(target)
qc.h(control)
qc.h(target)

qc.measure(control, cr[0])
qc.measure(target, cr[1])
return qc

qc_YY = measure_in_basis(qc.copy(), basis="YY")
display(
qc_YY.draw(output="mpl", fold=-1, scale=0.5)
) # Circuit for measuring in the YY basis

Ausgabe der vorherigen Code-Zelle

Alles zusammensetzen

Wir kombinieren die oben definierten Einzelschritte, um ein weitreichendes CX-Gate an den Enden einer 1D-Linie zu erstellen. Die Schritte umfassen:

  • Initialisierung des Kontroll-Qubits in ket+\\ket{+}
  • Vorbereitung von Bell-Paaren
  • Messung benachbarter Qubit-Paare
  • Anwendung von Feedforward-Korrekturen abhängig von den MCMs
def lrcx(distance, prep_barrier=True, pre_measure_barrier=True):
qc = initialize_circuit(distance)
qc = prepare_bell_pairs(qc, prep_barrier)
qc = measure_bell_basis(qc, pre_measure_barrier)
qc = apply_ffwd_corrections(qc)
return qc

qc = lrcx(distance)
# Apply the measurement in the XX, YY, and ZZ bases
qc_XX, qc_YY, qc_ZZ = [
measure_in_basis(qc, basis=basis) for basis in ["XX", "YY", "ZZ"]
]

display(
qc_YY.draw(output="mpl", fold=-1, scale=0.5)
) # Circuit for measuring in the YY basis

Ausgabe der vorherigen Code-Zelle

Circuits für verschiedene Abstände generieren

Wir generieren jetzt weitreichende CX-Circuits für eine Reihe von Qubit-Abständen. Für jeden Abstand erstellen wir Circuits, die in der XXXX-, YYYY- und ZZZZ-Basis messen — diese werden später zur Berechnung der Fidelity verwendet.

Die Liste der Abstände umfasst sowohl kurz- als auch langreichweitige Trennungen, wobei distance = 0 einem nächste-Nachbarn-CX entspricht. Diese Abstände werden auch verwendet, um die entsprechenden unitären Circuits zum Vergleich zu generieren.

distances = [
0,
1,
2,
3,
6,
11,
16,
21,
28,
35,
44,
55,
60,
] # Distances for long range CX. distance of 0 is a nearest-neighbor CX
distances.sort()
assert (
min(distances) >= 0
) # Only works for distance larger than 2 because classical register cannot be empty
basis_list = ["XX", "YY", "ZZ"]

circuits_dyn = []
for distance in distances:
for basis in basis_list:
circuits_dyn.append(
measure_in_basis(lrcx(distance, prep_barrier=False), basis=basis)
)
print(f"Number of circuits: {len(circuits_dyn)}")
circuits_dyn[14].draw(fold=-1, output="mpl", idle_wires=False)
Number of circuits: 39

Ausgabe der vorherigen Code-Zelle

Unitäre Implementierung durch Vertauschen der Qubits zur Mitte

Zum Vergleich betrachten wir zunächst den Fall, bei dem ein weitreichendes CNOT-Gate mit nächste-Nachbarn-Verbindungen und unitären Gates implementiert wird. In der folgenden Abbildung ist links ein Circuit für ein weitreichendes CNOT-Gate über eine 1D-Kette von n-Qubits mit ausschließlich nächste-Nachbarn-Verbindungen zu sehen. In der Mitte befindet sich eine äquivalente unitäre Zerlegung, die mit lokalen CNOT-Gates implementierbar ist, mit Circuit-Tiefe O(n)O(n).

Weitreichendes CNOT-Circuit

Der Circuit in der Mitte kann wie folgt implementiert werden:

def cnot_unitary(distance):
"""Generate a long range CNOT gate using local CNOTs on a 1D chain of qubits subject to n
nearest-neighbor connections only.

Args:
distance (int) : The distance of the CNOT gate, with the convention that a distance of 0 is a nearest-neighbor CNOT.

Returns:
QuantumCircuit: A Quantum Circuit implementing a long-range CNOT gate between qubit 0 and qubit distance+1
"""
assert distance >= 0
n = distance # number of qubits between target and control

qr = QuantumRegister(
n + 2, name="q"
) # Circuit with n qubits between control and target
cr = ClassicalRegister(
2, name="cr"
) # Classical register for measuring control and target qubits

qc = QuantumCircuit(qr, cr, name="CNOT_unitary")

control_qubit = 0

qc.h(control_qubit) # Prepare the control qubit in the |+> state

k = int(n / 2)
qc.barrier()
for i in range(control_qubit, control_qubit + k):
qc.cx(i, i + 1)
qc.cx(i + 1, i)
qc.cx(-i - 1, -i - 2)
qc.cx(-i - 2, -i - 1)
if n % 2 == 1:
qc.cx(k + 2, k + 1)
qc.cx(k + 1, k + 2)
qc.barrier()
qc.cx(k, k + 1)
for i in range(control_qubit, control_qubit + k):
qc.cx(k - i, k - 1 - i)
qc.cx(k - 1 - i, k - i)
qc.cx(k + i + 1, k + i + 2)
qc.cx(k + i + 2, k + i + 1)
if n % 2 == 1:
qc.cx(-2, -1)
qc.cx(-1, -2)

return qc

Nun werden alle unitären Circuits erstellt und die Circuits generiert, die in der XXXX-, YYYY- und ZZZZ-Basis messen — genau wie bei den dynamischen Circuits oben.

circuits_uni = []
for distance in distances:
for basis in basis_list:
circuits_uni.append(
measure_in_basis(cnot_unitary(distance), basis=basis)
)

print(f"Number of circuits: {len(circuits_uni)}")
circuits_uni[14].draw(fold=-1, output="mpl", idle_wires=False)
Number of circuits: 39

Ausgabe der vorherigen Code-Zelle

Da wir nun sowohl dynamische als auch unitäre Circuits für eine Reihe von Abständen haben, sind wir für die Transpilierung bereit. Wir müssen zunächst ein Backend-Gerät auswählen.

# Set up access to IBM Quantum devices
from qiskit.circuit import IfElseOp

service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=156
)

Der folgende Schritt stellt sicher, dass das Backend die if_else-Instruktion unterstützt, die für die neuere Version dynamischer Circuits erforderlich ist. Da diese Funktion sich noch in der frühen Zugriffsphase befindet, fügen wir IfElseOp dem Backend-Target explizit hinzu, falls es noch nicht verfügbar ist.

if "if_else" not in backend.target.operation_names:
backend.target.add_instruction(IfElseOp, name="if_else")

Layer-Fidelity-String zur Auswahl der 1D-Kette verwenden

Da wir die Performance von dynamischen und unitären Circuits auf einer 1D-Kette vergleichen möchten, verwenden wir den Layer-Fidelity-String, um eine lineare Topologie der besten Qubit-Kette aus dem Gerät auszuwählen. Dies stellt sicher, dass beide Circuit-Typen unter denselben Konnektivitätsbedingungen transpiliert werden und ein fairer Vergleich ihrer Performance möglich ist.

# This selects best qubits for longest distance and uses the same control for all lengths
lf_qubits = backend.properties().to_dict()[
"general_qlists"
] # best linear chain qubits
chosen_layouts = {
distance: [
val["qubits"]
for val in lf_qubits
if val["name"] == f"lf_{distances[-1] + 2}"
][0][: distance + 2]
for distance in distances
}
print(chosen_layouts[max(distances)]) # best qubits at each distance
[10, 11, 12, 13, 14, 15, 19, 35, 34, 33, 39, 53, 54, 55, 59, 75, 74, 73, 72, 71, 58, 51, 50, 49, 48, 47, 46, 45, 44, 43, 56, 63, 62, 61, 76, 81, 82, 83, 84, 85, 77, 65, 66, 67, 68, 69, 78, 89, 90, 91, 98, 111, 110, 109, 108, 107, 106, 105, 104, 103, 102, 101]
isa_circuits_dyn = []
isa_circuits_uni = []

# Using the same initial layouts for both circuits for better apples to apples comparison
for qc in circuits_dyn:
pm = generate_preset_pass_manager(
optimization_level=1,
backend=backend,
initial_layout=chosen_layouts[qc.num_qubits - 2],
)
isa_circuits_dyn.append(pm.run(qc))

for qc in circuits_uni:
pm = generate_preset_pass_manager(
optimization_level=1,
backend=backend,
initial_layout=chosen_layouts[qc.num_qubits - 2],
)
isa_circuits_uni.append(pm.run(qc))
print(
f"2Q depth: {isa_circuits_dyn[14].depth(lambda x: x.operation.num_qubits == 2)}"
)
isa_circuits_dyn[14].draw("mpl", fold=-1, idle_wires=0)
2Q depth: 2

Ausgabe der vorherigen Code-Zelle

print(
f"2Q depth: {isa_circuits_uni[14].depth(lambda x: x.operation.num_qubits == 2)}"
)
isa_circuits_uni[14].draw("mpl", fold=-1, idle_wires=False)
2Q depth: 13

Ausgabe der vorherigen Code-Zelle

Verwendete Qubits für den LRCX-Circuit visualisieren

In diesem Abschnitt untersuchen wir, wie der LRCX-Circuit auf Hardware abgebildet wird. Wir beginnen mit der Visualisierung der physischen Qubits im Circuit und analysieren dann, wie der Kontroll-Ziel-Abstand im Layout die Anzahl der Operationen beeinflusst.

# Note: the qubit coordinates must be hard-coded.
# The backend API does not currently provide this information directly.
# If using a different backend, you will need to adjust the coordinates accordingly,
# or set the qubit_coordinates = None to use the default layout coordinates.

def _heron_coords_r2():
"""Generate coordinates for the Heron layout in R2. Note"""
cord_map = np.array(
[
[
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
3,
7,
11,
15,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
1,
5,
9,
13,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
3,
7,
11,
15,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
1,
5,
9,
13,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
3,
7,
11,
15,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
1,
5,
9,
13,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
3,
7,
11,
15,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
],
-1
* np.array([j for i in range(15) for j in [i] * [16, 4][i % 2]]),
],
dtype=int,
)

hcords = []
ycords = cord_map[0]
xcords = cord_map[1]
for i in range(156):
hcords.append([xcords[i] + 1, np.abs(ycords[i]) + 1])

return hcords

# Visualize the active qubits in the circuit layout
plot_circuit_layout(
circuit=isa_circuits_uni[-1],
backend=backend,
view="physical",
qubit_coordinates=_heron_coords_r2(),
)

Ausgabe der vorherigen Code-Zelle

Schritt 3: Mit Qiskit-Primitiven ausführen

In diesem Schritt führen wir das Experiment auf dem angegebenen Backend aus. Wir verwenden dabei Batching, um das Experiment effizient über mehrere Durchläufe hinweg auszuführen. Wiederholte Durchläufe ermöglichen es uns, Mittelwerte für einen genaueren Vergleich zwischen unitären und dynamischen Methoden zu berechnen und deren Variabilität durch den Vergleich der Abweichungen zwischen den Durchläufen zu quantifizieren.

print(backend.name)
ibm_kingston

Anzahl der Durchläufe auswählen und Batch-Ausführung durchführen.

num_trials = 10
jobs_uni = []
jobs_dyn = []
with Batch(backend=backend) as batch:
sampler = Sampler(mode=batch)
for _ in range(num_trials):
jobs_uni.append(sampler.run(isa_circuits_uni, shots=1024))
jobs_dyn.append(sampler.run(isa_circuits_dyn, shots=1024))

Schritt 4: Nachverarbeitung und Rückgabe des Ergebnisses im gewünschten klassischen Format

Nachdem die Experimente erfolgreich ausgeführt wurden, verarbeiten wir jetzt die Messzählungen, um aussagekräftige Metriken zu extrahieren. In diesem Schritt werden wir:

  • Qualitätsmetriken zur Bewertung der Performance des weitreichenden CX definieren.
  • Erwartungswerte von Pauli-Operatoren aus rohen Messergebnissen berechnen.
  • Diese verwenden, um die Fidelity des erzeugten Bell-Zustands zu berechnen.

Diese Analyse liefert ein klares Bild davon, wie gut die dynamischen Circuits im Vergleich zur unitären Basisimplementierung abschneiden.

Qualitätsmetriken

Um den Erfolg des weitreichenden CX-Protokolls zu bewerten, messen wir, wie nah der Ausgabezustand am idealen Bell-Zustand ist. Eine praktische Möglichkeit zur Quantifizierung ist die Berechnung der Zustandsfidelity mithilfe der Erwartungswerte von Pauli-Operatoren. Die Fidelity für einen Bell-Zustand auf dem Kontroll- und Zielzustand kann berechnet werden, nachdem XX\braket{XX}, YY\braket{YY} und ZZ\braket{ZZ} bekannt sind. Insbesondere gilt:

F=14(1+XXYY+ZZ) F = \frac{1}{4} (1 + \braket{XX} - \braket{YY} + \braket{ZZ})

Um diese Erwartungswerte aus rohen Messdaten zu berechnen, definieren wir eine Reihe von Hilfsfunktionen:

  • compute_ZZ_expectation: Berechnet den Erwartungswert eines Zwei-Qubit-Pauli-Operators in der ZZ-Basis aus Messzählungen.
  • compute_fidelity: Kombiniert die Erwartungswerte von XXXX, YYYY und ZZZZ in den obigen Fidelity-Ausdruck.
  • get_counts_from_bitarray: Hilfsprogramm zum Extrahieren von Zählungen aus Backend-Ergebnisobjekten.
def compute_ZZ_expectation(counts):
total = sum(counts.values())
expectation = 0
for bitstring, count in counts.items():
# Ensure bitstring is 2 bits
z1 = (-1) ** (int(bitstring[-1]))
z2 = (-1) ** (int(bitstring[-2]))
expectation += z1 * z2 * count
return expectation / total

def compute_fidelity(counts_xx, counts_yy, counts_zz):
xx, yy, zz = [
compute_ZZ_expectation(c) for c in [counts_xx, counts_yy, counts_zz]
]
return 1 / 4 * (1 + xx - yy + zz)

Wir berechnen die Fidelity für die dynamischen weitreichenden CX-Circuits. Für jeden Abstand extrahieren wir Messergebnisse in den XX\braket{XX}-, YY\braket{YY}- und ZZ\braket{ZZ}-Basen. Diese Ergebnisse werden mithilfe der zuvor definierten Hilfsfunktionen kombiniert, um die Fidelity gemäß F=14(1+XXYY+ZZ)F = \tfrac{1}{4} \big( 1 + \langle XX \rangle - \langle YY \rangle + \langle ZZ \rangle \big) zu berechnen. Dies liefert die beobachtete Fidelity des dynamisch ausgeführten Protokolls bei jedem Abstand.

fidelities_dyn = []

# loop over trials
for job in jobs_dyn:
result_dyn = job.result()
trial_fidelities = []
# loop over all distances
for ind, dist in enumerate(distances):
counts_xx = result_dyn[ind * 3].data.cr.get_counts()
counts_yy = result_dyn[ind * 3 + 1].data.cr.get_counts()
counts_zz = result_dyn[ind * 3 + 2].data.cr.get_counts()
trial_fidelities.append(
compute_fidelity(counts_xx, counts_yy, counts_zz)
)
fidelities_dyn.append(trial_fidelities)
# average over trials for each distance
avg_fidelities_dyn = np.mean(fidelities_dyn, axis=0)
std_fidelities_dyn = np.std(fidelities_dyn, axis=0)

Nun berechnen wir die Fidelity für die unitären weitreichenden CX-Circuits auf die gleiche Weise wie für die dynamischen Circuits oben.

fidelities_uni = []

# loop over trials
for job in jobs_uni:
result_uni = job.result()
trial_fidelities = []
# loop over all distances
for ind, dist in enumerate(distances):
counts_xx = result_uni[ind * 3].data.cr.get_counts()
counts_yy = result_uni[ind * 3 + 1].data.cr.get_counts()
counts_zz = result_uni[ind * 3 + 2].data.cr.get_counts()
trial_fidelities.append(
compute_fidelity(counts_xx, counts_yy, counts_zz)
)
fidelities_uni.append(trial_fidelities)
# average over trials for each distance
avg_fidelities_uni = np.mean(fidelities_uni, axis=0)
std_fidelities_uni = np.std(fidelities_uni, axis=0)

Ergebnisse visualisieren

Um die Ergebnisse visuell zu veranschaulichen, zeichnet die folgende Zelle die geschätzten Gate-Fidelities bei variierendem Abstand zwischen verschränkten Qubits für die jeweiligen Methoden.

fig, ax = plt.subplots()

# Unitary with error bars
ax.errorbar(
distances,
avg_fidelities_uni,
yerr=std_fidelities_uni,
fmt="o-.",
color="c",
ecolor="c",
elinewidth=1,
capsize=4,
label="Unitary",
)
# Dynamic with error bars
ax.errorbar(
distances,
avg_fidelities_dyn,
yerr=std_fidelities_dyn,
fmt="o-.",
color="m",
ecolor="m",
elinewidth=1,
capsize=4,
label="Dynamic",
)
# Random gate baseline
ax.axhline(y=1 / 4, linestyle="--", color="gray", label="Random gate")

legend = ax.legend(frameon=True)
for text in legend.get_texts():
text.set_color("black")
legend.get_frame().set_facecolor("white")
legend.get_frame().set_edgecolor("black")
ax.set_title(
"Bell State Fidelity vs Control–Target Separation", color="black"
)
ax.set_xlabel("Distance", color="black")
ax.set_ylabel("Bell state fidelity", color="black")
ax.grid(linestyle=":", linewidth=0.6, alpha=0.4, color="gray")
ax.set_ylim((0.2, 1))
ax.set_facecolor("white")
fig.patch.set_facecolor("white")
for spine in ax.spines.values():
spine.set_visible(True)
spine.set_color("black")
ax.tick_params(axis="x", colors="black")
ax.tick_params(axis="y", colors="black")
plt.show()

Ausgabe der vorherigen Code-Zelle

Aus dem obigen Fidelity-Diagramm geht hervor, dass das LRCX die direkte unitäre Implementierung nicht durchgängig übertroffen hat. Tatsächlich erzielte der unitäre Circuit bei kurzen Kontroll-Ziel-Abständen eine höhere Fidelity. Bei größeren Abständen beginnt der dynamische Circuit jedoch bessere Fidelity als die unitäre Implementierung zu erreichen. Dieses Verhalten ist auf aktueller Hardware nicht unerwartet: Während dynamische Circuits die Circuit-Tiefe durch die Vermeidung langer SWAP-Ketten reduzieren, führen sie zusätzliche Circuit-Zeit durch Mid-Circuit-Messungen, klassisches Feedforward und Steuerpfad-Verzögerungen ein. Die zusätzliche Latenz erhöht Dekohärenz und Auslesefehler, was bei kurzen Abständen die Tiefeneinsparungen überwiegen kann.

Dennoch beobachten wir einen Kreuzungspunkt, an dem der dynamische Ansatz den unitären übertrifft. Dies ist ein direktes Ergebnis der unterschiedlichen Skalierung: Die Tiefe des unitären Circuits wächst linear mit dem Abstand zwischen den Qubits, während die Tiefe des dynamischen Circuits konstant bleibt.

Wichtige Punkte:

  • Unmittelbarer Vorteil dynamischer Circuits: Die hauptsächliche aktuelle Motivation ist die reduzierte Zwei-Qubit-Tiefe, nicht zwingend verbesserte Fidelity.
  • Warum die Fidelity heute schlechter sein kann: Erhöhte Circuit-Zeit durch Mess- und klassische Operationen dominiert oft, besonders wenn der Kontroll-Ziel-Abstand klein ist.
  • Ausblick: Mit verbesserter Hardware — insbesondere schnellerem Auslesen, kürzerer klassischer Steuerlatenz und reduziertem Mid-Circuit-Overhead — sollten diese Tiefen- und Laufzeitreduzierungen zu messbaren Fidelity-Gewinnen führen.
# Compute metrics for each distance, skipping the basis circuits since they are identical for each distance
depths_2q_dyn = [
c.depth(lambda x: x.operation.num_qubits == 2)
for c in isa_circuits_dyn[::3]
]
meas_dyn = [
sum(1 for instr in c.data if instr.operation.name == "measure")
for c in isa_circuits_dyn[::3]
]

depths_2q_uni = [
c.depth(lambda x: x.operation.num_qubits == 2)
for c in isa_circuits_uni[::3]
]
meas_uni = [
sum(1 for instr in c.data if instr.operation.name == "measure")
for c in isa_circuits_uni[::3]
]

fig, axes = plt.subplots(1, 2, figsize=(12, 5))

axes[0].plot(
distances, depths_2q_uni, "o-.", color="c", label="Unitary (2Q depth)"
)
axes[0].plot(
distances, depths_2q_dyn, "o-.", color="m", label="Dynamic (2Q depth)"
)
axes[0].set_xlabel("Number of qubits between control and target")
axes[0].set_ylabel("Two-qubit depth")
axes[0].grid(True, linestyle=":", linewidth=0.6, alpha=0.4)
axes[0].legend()

axes[1].plot(
distances, meas_uni, "o-.", color="c", label="Unitary (# measurements)"
)
axes[1].plot(
distances, meas_dyn, "o-.", color="m", label="Dynamic (# measurements)"
)
axes[1].set_xlabel("Number of qubits between control and target")
axes[1].set_ylabel("Number of measurements")
axes[1].grid(True, linestyle=":", linewidth=0.6, alpha=0.4)
axes[1].legend()

fig.suptitle("Scaling of Unitary vs Dynamic LRCX with Distance", fontsize=12)

plt.tight_layout()
plt.show()

Ausgabe der vorherigen Code-Zelle

Dieses Zwei-Qubit-Tiefendiagramm hebt den primären Vorteil des mit dynamischen Circuits implementierten LRCX hervor: Die Performance bleibt im Wesentlichen konstant, wenn der Abstand zwischen Kontroll- und Ziel-Qubit zunimmt. Die unitäre Implementierung hingegen wächst aufgrund der erforderlichen SWAP-Ketten linear mit dem Abstand. Die Tiefe erfasst die logische Skalierung von Zwei-Qubit-Operationen, während die Messzählung den zusätzlichen Overhead für dynamische Circuits widerspiegelt. Diese Messungen sind effizient, da sie parallel durchgeführt werden, führen aber auf heutiger Hardware dennoch zu fixen Kosten.

Warum die Fidelity heute schlechter sein kann: Erhöhte Circuit-Zeit durch Mess- und klassische Operationen dominiert oft, besonders wenn der Kontroll-Ziel-Abstand klein ist. Beispielsweise beträgt die durchschnittliche Ausleselänge auf einem Heron-r2-Prozessor 2.280 ns, während seine 2Q-Gate-Länge nur 68 ns beträgt.

Mit verbesserter Mess- und klassischer Latenz erwarten wir, dass die konstante Tiefen- und Messskalierung dynamischer Circuits klare Fidelity- und Laufzeitvorteile bei größeren Circuits liefern wird.

Referenzen

[1] Efficient Long-Range Entanglement using Dynamic Circuits, von Elisa Bäumer, Vinay Tripathi, Derek S. Wang, Patrick Rall, Edward H. Chen, Swarnadeep Majumder, Alireza Seif, Zlatko K. Minev. IBM Quantum, (2023). https://arxiv.org/abs/2308.13065