Langstrecken-Verstriemelung mit dynamische Schaltkresen
Bruukstied-Schattung: 4 Minuten up'n Heron r2-Prozessor. (OPPASSEN: Dat is blot en Schattung. Bi di kann dat länger or körter duern.)
Achtergrun
Langstrecken-Verstriemelung twischen wied vun'nanner liggende Qubits is en grote Ruutforderung up Gereden mit beperkte Konnektivität. Dit Tutorial wiest, wo dynamische Schaltkresen so wat genereren köönt, indeem wi en Langstrecken-Controlled-X-Gatter (LRCX) över en metungsbaseerd Protokoll implementeert.
Na den Ansatz vun Elisa Bäumer et al. in 1 bruukt de Methood Mid-Circuit-Metungen un Feedforward, üm konstante Gatterdeepte to kriegen, ganz liek, wo wied de Qubits vun'nanner sünd. Se maakt Twischenschritt-Bell-Poren, mitt een Qubit vun elk Paar un wendt klassisch bedüngte Gatters an, üm de Verstriemelung över dat Gereed to verbreden. Dat vermitt lange SWAP-Keden un reduzeert dormit de Schaltkreisdeepte un de Anfälligkeet för Twee-Qubit-Gatterfehlers.
In dit Notebook passt wi dat Protokoll för IBM Quantum®-Hardware an un wiedern dat ut, üm mehr LRCX-Operatschonen parallel lopen to laten, wat uns verlövt to ünnersöken, wo de Performance mit de Tahl vun gliektiedige bedüngte Operatschonen skaleert.
Vöruttsetungen
Bevör du mit dit Tutorial anfängst, stell seker, dat du Folgendes installeert hest:
- Qiskit SDK v2.0 or later, mit Visualisierung
- Qiskit Runtime (
pip install qiskit-ibm-runtime) v0.37 or later
Setup
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 Ingaven up en Quanten-Problem afbillen
Wi implementeert nu en Langstrecken-CNOT-Gatter twischen twee wiede Qubits, na de dynamische Schaltkreis-Konstruktschon, de ünnen wiest warrt (anpasst vun Fig. 1a ut Ref. 1). De zentrale Idee is, en "Bus" vun Ancilla-Qubits, initialiseert mit , to bruken, üm langstrecken Gate-Teleportation to vermitteln.

As in dat Bild wiest, löppt de Prozess so:
- Bereidt en Kede vun Bell-Poren vör, de de Control- un Target-Qubits över Twischenancillas verbinnt.
- Führt Bell-Metungen twischen nich-verstrieemelte Naverqubits dör, swappt Verstriemelung Schritt för Schritt, bit Control un Target en Bell-Paar deelt.
- Bruukt dat Bell-Paar för Gate-Teleportation, wandelt en lokales CNOT in en deterministisch Langstrecken-CNOT in konstante Deepte üm.
De Ansatz vervangt lange SWAP-Keden mit en konstante Deepte-Protokoll, reduzeert de Anfälligkeet för Twee-Qubit-Gatterfehlers un maakt de Operatschon skaleerbar mit de Gereedgröte.
In't Folgende gahn wi eerst dör de dynamische Schaltkreis-Implementierung vun den LRCX-Schaltkreis. An't Enn stelln wi ok en unitäre Implementatschon för Verglieken vör, üm de Vördeels vun dynamische Schaltkresen in dit Setting to wiesen.
(i) Schaltkreis initialiseren
Wi fangt mit en eenfach Quanten-Problem an, dat as Basis för Verglieken deent. Konkret initialiseren wi en Schaltkreis mit en Control-Qubit bi Index 0 un leggt en Hadamard-Gatter dor up. Dat produzeert en Superpositions-Tostand, de, wenn na en Controlled-X-Operatschon folgt, en Bell-Tostand twischen Control- un Target-Qubits genereert.
Bi dissen Punkt konstrueren wi noch nich dat Langstrecken-Controlled-X (LRCX) sülvst. In Steed is unse Teel, en kloren un minimalen Anfangsschaltkreis to defineren, de de Rull vun't LRCX upwiest. In Schritt 2 wiest wi, wo dat LRCX as Optimierung mit dynamische Schaltkresen implementeert warrn kann un vergliekt sien Performance gegen en unitär Äquivalent. Wichtig is, dat dat LRCX-Protokoll up elk Anfangsschaltkreis anwennt warrn kann. Hier bruken wi dit eenfache Hadamard-Setup för klore Demonstratschon.
distance = 6 # De Distanz vun't CNOT-Gatter, mit de Konventschon, dat en Distanz vun null en nächst-Naver-CNOT is.
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)
Schritt 2: Problem för Quantenhardware-Utföhrung optimeren
In dissen Schritt wiest wi, wo man den LRCX-Schaltkreis mit dynamische Schaltkresen konstrueert. Dat Teel is, den Schaltkreis för de Utföhrung up Hardware to optimeren, indeem wi de Deepte in Verglieken to en rein unitäre Implementatschon reduzeren. Üm de Vördeels to illustreren, wiest wi sowohl de dynamische LRCX-Konstruktschon as ok ehr unitär Äquivalent un vergliekt later ehr Performance na Transpilatschon. Wichtig is, dat wi hier dat LRCX up en eenfach Hadamard-initialiseert Problem anwennt, dat Protokoll kann aver up elk Schaltkreis anwennt warrn, wo en Langstrecken-CNOT nödig is.
(ii) Bell-Poren vörbereeden
Wi fangt dormit an, en Kede vun Bell-Poren längs den Padd twischen Control- un Target-Qubits to erstellen. Falls de Distanz ungerood is, wendt wi eerst en CNOT vun Control to sien Naver an, dat is dat CNOT, dat teleporteert warrt. För en gerode Distanz warrt dat CNOT na den Bell-Paar-Vörbereeden-Schritt anwennt. De Bell-Paar-Kede verstriemelt dann na'nanner liggende Qubit-Poren un etableert de Ressource, de bruukt warrt, üm de Control-Informatschon över dat Gereed to dregen.
# 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)
(iii) Naverqubit-Poren in de Bell-Basis meten
As nächstes mett wi nich-verstrieemelte Naverqubits in de Bell-Basis (Twee-Qubit-Metungen vun un ). Dat kreet en Langstrecken-Bell-Paar twischen dat Target-Qubit un dat Qubit neven dat Control (bit up Pauli-Korrekturen, de över Feedforward in'n nächsten Schritt implementeert warrt). Parallel dorto implementeren wi de verstrieemelnde Metung, de dat CNOT-Gatter teleporteert, üm up dat beabsichtigte Target-Qubit to warken.
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)
(iv) As nächstes Feedforward-Korrekturen anwennen, üm Pauli-Byprodukt-Operatoren to korrigeren
De Bell-Basis-Metungen föhrt Pauli-Byprodukten in, de mit de upnahmen Resultaten korrigeert warrn mööt. Dat geiht in twee Schritten. Eerst mööt wi de Parität vun all de -Metungen berekenen, de dann bruukt warrt, üm bedüngt en -Gatter up dat Target-Qubit antowennen. Liekso warrt de Parität vun de -Metungen berekent un bruukt, üm bedüngt en -Gatter up dat Control-Qubit antowennen.
Mit dat nie klassische Expression-Framework in Qiskit köönt disse Paritäten direkt in de klassische Verarbenns-Schicht vun den Schaltkreis berekent warrn. In Steed vun en Reeg vun enkelte bedüngte Gatters för elk Metungs-Bit antowennen, köönt wi en enkelte klassische Expression boen, de dat XOR (Parität) vun all relevanten Metungs-Resultaten darstellt. Disse Expression warrt dann as Bedüngung in en enkelt if_test-Block bruukt, wat dat de Korrektur-Gatters verlövt, in konstante Deepte anwennt to warrn. De Ansatz vereinfacht sowohl den Schaltkreis as ok stellt seker, dat de Feedforward-Korrekturen keen onnötige tösätzliche Latenz inföhrt.
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)
(v) To't Sluss Control- un Target-Qubits meten
Wi defineren en Helper-Funktschon, de dat möglich maakt, Control- un Target-Qubits in de -, - or -Basen to meten. För de Verifizierung vun den Bell-Tostand schöölt de Verwachtungswerten vun un beide sien, do se Stabilisatoren vun den Tostand sünd. De -Metung warrt hier ok ünnerstött un warrt ünnen bruukt, wenn wi de Fidelity berekent.
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
Dat ganze tosamen setten
Wi kombineren de verscheden Schritten vun baven, üm en Langstrecken-CX-Gatter up twee Ennen vun en 1D-Linie to kreern. De Schritten sünd
- Dat Control-Qubit in initialiseren
- Bell-Poren vörbereeden
- Naverqubit-Poren meten
- Feedforward-Korrekturen afhängig vun de MCMs anwennen
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
Schaltkresen för verscheden Distanzen genereren
Wi generert nu Langstrecken-CX-Schaltkresen för en Bereich vun Qubit-Trennungen. För elke Distanz boot wi Schaltkresen, de in de -, - un -Basen mett, de later bruukt warrt, üm Fidelitys to berekenen.
De List vun Distanzen enthöllt sowohl korte as ok lange Trennungen, wobei distance = 0 en nächst-Naver-CX entspreekt. Disse sülvigen Distanzen warrt ok later bruukt, üm de entsprekenden unitären Schaltkresen för Verglieken to genereren.
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
Unitäre Implementatschon, de de Qubits na de Mitt swappt
För Verglieken ünnersökt wi eerst den Fall, wo en Langstrecken-CNOT-Gatter mit nächst-Naver-Verbinnungen un unitäre Gatters implementeert warrt. In dat folgende Bild is links en Schaltkreis för en Langstrecken-CNOT-Gatter, dat en 1D-Kede vun n-Qubits överspannt, wobei blot nächst-Naver-Verbinnungen bruukt warrt. In de Mitt is en äquivalente unitäre Opsplittung, de mit lokale CNOT-Gatters implementeerbar is, Schaltkreisdeepte .

De Schaltkreis in de Mitt kann so implementeert warrn:
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
Nu boot wi all unitären Schaltkresen un boot de Schaltkresen, de in de -, - un -Basen mett, liekso as wi dat för de dynamischen Schaltkresen baven maakt hebbt.
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
Nu, wo wi sowohl dynamische as ok unitäre Schaltkresen för en Bereich vun Distanzen hebbt, sünd wi praat för Transpilatschon. Wi mööt eerst en Backend-Gereed utwä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
)
De folgende Schritt stellt seker, dat dat Backend de if_else-Instrukschon ünnerstött, de för de niere Versjoon vun dynamische Schaltkresen bruukt warrt. Do disse Funktschon noch in Early Access is, föögt wi dat IfElseOp explizit to't Backend-Target to, falls dat noch nich verfögbar is.
if "if_else" not in backend.target.operation_names:
backend.target.add_instruction(IfElseOp, name="if_else")
Layer Fidelity String för Utwahl vun en 1D-Kede bruken
Do wi de Performance vun dynamische un unitäre Schaltkresen up en 1D-Kede verglieken wüllt, brukt wi den Layer Fidelity String, üm en lineare Topologie vun de besten Kede vun Qubits ut dat Gereed uttowählen. Dat stellt seker, dat beide Sorten vun Schaltkresen ünner de sülvigen Konnektivitäts-Beperkungen transpileert warrt, wat en foren Verglieken vun ehr Performance verlövt.
# 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

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

Qubits visualiseren, de för den LRCX-Schaltkreis bruukt warrt
In dissen Afsnitt ünnersökt wi, wo de LRCX-Schaltkreis up Hardware ümsett warrt. Wi fangt dormit an, de physischen Qubits to visualiseren, de in den Schaltkreis bruukt warrt, un dann studert wi, wo de Control–Target-Distanz in't Layout de Tahl vun Operatschonen beinflusst.
# 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(),
)

Schritt 3: Mit Qiskit-Primitiven utföhren
In dissen Schritt föhrt wi dat Experiment up dat aangeven Backend ut. Wi brukt ok Batching, üm dat Experiment efficient över mehr Trials lopen to laten. Wedderholt Trials verlööft uns, Döörsnittswerten för en genauen Verglieken twischen de unitäre un dynamische Methoden to berekenen, un ok ehr Variabilität to quantifizeren, indeem wi de Afwieken över Runs vergliekt.
print(backend.name)
ibm_kingston
Wähl Tahl vun Trials ut un föhr Batch-Utföhrung dör.
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: Naverarbeeden un Resultaten in dat wünscht klassische Format trüchgeven
Nadem de Experimenten erfolgriek utföhrt worrn sünd, verarbeidt wi nu de Metungs-Counts na, üm sinnvolle Metriken to extraheren. In dissen Schritt:
- Definert wi Qualitäts-Metriken för de Evaluierung vun de Performance vun den Langstrecken-CX.
- Berekent wi Verwachtungswerten vun Pauli-Operatoren ut rohe Metungs-Resultaten.
- Brukt wi disse, üm de Fidelity vun den genereerten Bell-Tostand to berekenen.
Disse Analyse gifft uns en klor Bild dorvun, wo goot de dynamischen Schaltkresen relativ to de unitäre Baseline-Implementatschon afsniet.
Qualitäts-Metriken
Üm den Erfolg vun't Langstrecken-CX-Protokoll to evalueren, mett wi, wo nah de Output-Tostand an den idealen Bell-Tostand is. En bequeme Oort, dat to quantifizeren, is de State-Fidelity mit Verwachtungswerten vun Pauli-Operatoren to berekenen. Fidelity för en Bell-Tostand up den Control- un Target-Tostand kann berekent warrn, nadem wi dat , un weet. Konkret,
Üm disse Verwachtungswerten ut rohe Metungs-Daten to berekenen, definert wi en Sett vun Helper-Funktschonen:
compute_ZZ_expectation: Berekent, geven Metungs-Counts, den Verwachtungswert vun en Twee-Qubit-Pauli-Operator in de -Basis.compute_fidelity: Kombineert de Verwachtungswerten vun , un in de Fidelity-Expression baven.get_counts_from_bitarray: Helper, üm Counts ut Backend-Resultaten-Objekten to extraheren.
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)
Wi berekent de Fidelity för de dynamischen Langstrecken-CX-Schaltkresen. För elke Distanz extrahert wi Metungs-Resultaten in de -, - un -Basen. Disse Resultaten warrt mit de vörher definierten Helper-Funktschonen kombineert, üm de Fidelity na to berekenen. Dat gifft de beobachten Fidelity vun't dynamisch utföhrten Protokoll bi elke Distanz.
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)
Nu berekent wi de Fidelity för de unitären Langstrecken-CX-Schaltkresen, un wi maakt dat liekso as för de dynamischen Schaltkresen baven.
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)
De Resultaten plotten
Üm de Resultaten visuell to würdigen, plottt de Cell ünnen de schatteten Gatter-Fidelitys, de bi verscheden Distanzen twischen verstrieemelte Qubits för de Methoden meten worrn sünd.
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()

Ut den Fidelity-Plot baven hett dat LRCX nich konstant beter afsneden as de direkte unitäre Implementatschon. In Fakt, för korte Control–Target-Trennungen hett de unitäre Schaltkreis en högere Fidelity erreekt. Aver bi grötere Trennungen fangt de dynamische Schaltkreis an, betere Fidelity as de unitäre Implementatschon to erreen. Dat Verholten is nich unverwacht up aktuelle Hardware: Während dynamische Schaltkresen de Schaltkreisdeepte reduzert, indeem se lange SWAP-Keden vermiett, föhrt se tösätzliche Schaltkreistied ut Mid-Circuit-Metungen, klassischen Feedforward un Control-Path-Vertögerungen in. De tösätzliche Latenz erhöht Dekohärenz un Readout-Fehlers, wat de Deepte-Sparungen bi korte Distanzen överwegen kann.
Trotzdem beobacht wi en Crossover-Punkt, wo de dynamische Ansatz den unitären överwinn. Dat is en direktes Resultat vun de verscheden Skaleren: De Deepte vun den unitären Schaltkreis wassen linear mit de Distanz twischen Qubits, während de Deepte vun den dynamischen Schaltkreis konstant blifft.
Zentrale Punkten:
- Direkten Vördeel vun dynamische Schaltkresen: De hööftsächliche hüüdige Motivatschon is reduzeerde Twee-Qubit-Deepte, nich nödigerwies verbeterte Fidelity.
- Worüm Fidelity hüüt slech ter sien kann: Erhöhte Schaltkreistied ut Metung un klassische Operatschonen domineert vaken, besünners wenn de Control–Target-Trennung lütt is.
- Kiek na vörut: Mit betere Hardware, besünners snelleren Readout, körtere klassische Control-Latenz un reduzeerden Mid-Circuit-Overhead, schöölt wi verwachten, dat disse Deepte- un Duer-Reduzeerungen sik in meetbare Fidelity-Winn ümsett.
# 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()

Disse Twee-Qubit-Deepte-Plot hevt den hööftsächlichen Vördeel vun't LRCX, implementeert mit dynamische Schaltkresen, rut: De Performance blifft in't Wesentliche konstant, as de Trennung twischen Control- un Target-Qubits gröter warrt. In Gegensatz dorto wassen de unitäre Implementatschon linear mit de Distanz wegen de bruukten SWAP-Keden. Deepte fangt de logische Skaleren vun Twee-Qubit-Operatschonen, während de Metungs-Tahl den tösätzlichen Overhead för dynamische Schaltkresen spegelt. Disse Metungen sünd efficient, do se parallel döörföhrt warrt, aver se föhrt trotzdem fixe Kosten up hüüdige Hardware in.
Worüm Fidelity hüüt slechter sien kann: Erhöhte Schaltkreistied ut Metung un klassische Operatschonen domineert vaken, besünners wenn de Control-Target-Trennung lütt is. To'n Bispill, de döörsnittliche Readout-Längt up en Heron r2-Prozessor is 2.280 ns, während sien 2Q-Gatterlängt blot 68 ns is.
Mit betere Metungs- un klassische Latenzen verwacht wi, dat de konstante Deepte- un konstante Metungs-Skaleren vun dynamische Schaltkresen klore Fidelity- un Runtime-Vördeels up grötere Schaltkresen levern warrt.
Referenzen
[1] Efficient Long-Range Entanglement using Dynamic Circuits, by 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