Skip to main content
IBM Quantum Platform

Langreichweitige Verschränkung mit dynamischen Schaltkreisen

Geschätzte Nutzungsdauer: 4 Minuten auf einem Heron r2 Prozessor. (HINWEIS: Dies ist nur eine Schätzung. Ihre Laufzeit kann variieren.)


Lernziele

  • Wie man ein CNOT-Gatter mit großer Reichweite mithilfe dynamischer Schaltungen mit Messungen während des Schaltvorgangs (MCMs) und klassischer Vorwärtskopplung implementiert;
  • Wie man das äquivalente Gatter mithilfe eines auf dem unitären SWAP-Operator basierenden Ansatzes implementiert;
  • Wie lassen sich beide Ansätze vergleichen, indem man die Gate-Genauigkeit als Funktion des Qubit-Abstands misst?

Voraussetzungen


Hintergrund

Die Verschränkung über große Entfernungen zwischen weit entfernten Qubits ist auf Geräten mit begrenzter Konnektivität eine Herausforderung. Dieses Tutorial zeigt, wie dynamische Schaltkreise eine solche Verschränkung erzeugen können, indem sie ein Long-Range-Controlled-X-Gate (LRCX) mit einem messungsbasierten Protokoll implementieren.

In Anlehnung an den Ansatz von Elisa Bäumer et al. ( 1) nutzt die Methode die Messung in der Mitte des Schaltkreises und die Vorwärtssteuerung, um Gatter mit konstanter Tiefe unabhängig vom Qubitabstand zu erreichen. Es erzeugt dazwischenliegende Bell-Paare, misst ein Qubit von jedem Paar und wendet klassisch konditionierte Gatter an, um die Verschränkung über das Gerät zu übertragen. Dadurch werden lange SWAP-Ketten vermieden und sowohl die Schaltungstiefe als auch die Anfälligkeit für Zwei-Qubit-Gatefehler verringert.

In diesem Notizbuch passen wir das Protokoll für die „ IBM Quantum “-Hardware an und messen deren Leistung in Abhängigkeit vom Abstand zwischen Steuerung und Ziel, wobei wir einen Vergleich mit einer auf dem unitaren SWAP-Verfahren basierenden Referenzlösung anstellen.


Anforderungen

Vergewissern Sie sich vor Beginn dieses Tutorials, dass Sie die folgenden Komponenten installiert haben:

  • Qiskit SDK v2.0 oder später, mit Visual isierungsunterstützung
  • Qiskit Runtime v0.37 oder später (pip install qiskit-ibm-runtime)
  • Qiskit Aer v0.17 oder höher (pip install qiskit-aer)

Einrichtung

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

Beispiel für einen Simulator im kleinen Maßstab

Bevor wir den Code auf der echten QPU ausführen, überprüfen wir auf einem rauschfreien Simulator, ob sowohl die dynamischen als auch die unitären Schaltkreise einen idealen Bell-Zustand erzeugen. Wir verwenden den Modus „ IBM Quantum “ Sampler mit dem Backend-Modus AerSimulator „as“ und einem geringen Abstand von 6.

Schritt 1: Klassische Eingaben einem Quantenproblem zuordnen

Wir realisieren nun ein CNOT-Gatter mit großer Reichweite zwischen zwei weit voneinander entfernten Qubits gemäß dem unten dargestellten dynamischen Schaltkreis (in Anlehnung an Abb. 1a in Ref. 1 ). Der Kerngedanke besteht darin, einen „Bus“ aus Ancilla-Qubits, die auf den Zustand ∣0⟩|0\rangle initialisiert sind, zur Vermittlung der Fern-Gatter-Teleportation zu nutzen.

CNOT-Schaltung mit großer Reichweite

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

  1. Bereiten Sie eine Kette von Bell-Paaren vor, die die Kontroll- und Ziel-Qubits über dazwischenliegende Ancillas verbinden.
  2. Führen Sie Bell-Messungen zwischen nicht verschränkten benachbarten Qubits durch und tauschen Sie die Verschränkung schrittweise aus, bis Kontrolle und Ziel ein Bell-Paar haben.
  3. Verwenden Sie dieses Bell-Paar für die Tor-Teleportation und verwandeln Sie ein lokales CNOT in ein deterministisches CNOT mit großer Reichweite und konstanter Tiefe.

Dieser Ansatz ersetzt lange SWAP-Ketten durch ein Protokoll mit konstanter Tiefe, wodurch die Anfälligkeit für Zwei-Qubit-Gate-Fehler verringert wird und die Operation mit der Größe des Bauelements skalierbar wird.

Im Folgenden werden wir zunächst die dynamische Schaltungsimplementierung der LRCX-Schaltung erläutern. Am Ende werden wir auch eine unitäre Implementierung zum Vergleich bereitstellen, um die Vorteile dynamischer Schaltungen in dieser Umgebung hervorzuheben.

Schaltung initialisieren

Wir beginnen mit einem einfachen Quantenproblem, das als Vergleichsgrundlage dienen soll. Konkret initialisieren wir einen Schaltkreis mit einem Steuerqubit am Index 0 und wenden darauf ein Hadamard-Gatter an. Dadurch entsteht ein Überlagerungszustand, der bei anschließender Durchführung einer kontrollierten X-Operation einen Bell-Zustand (∣00⟩+∣11⟩)/2(|00\rangle + |11\rangle)/\sqrt{2} zwischen dem Steuer- und dem Ziel-Qubit erzeugt.

Zum gegenwärtigen Zeitpunkt ist die Konstruktion des Langstrecken-Kontrollsystems LRCX noch nicht abgeschlossen. Unser Ziel ist es vielmehr, eine klare und minimale Ausgangsschaltung zu definieren, die die Rolle des LRCX hervorhebt. In Schritt 2 zeigen wir, wie LRCX als Optimierung mit dynamischen Schaltungen implementiert werden kann, und vergleichen seine Leistung mit der eines unitären Äquivalents. Wichtig ist, dass das LRCX-Protokoll auf jede Ausgangsschaltung angewendet werden kann. Zur Veranschaulichung verwenden wir hier diesen einfachen Hadamard-Aufbau.

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)

Output:

Output of the previous code cell

Schritt 2: Optimierung des Problems für die Ausführung auf Quantenhardware

In diesem Schritt zeigen wir, wie die LRCX-Schaltung mit Hilfe dynamischer Schaltungen aufgebaut wird. Ziel ist es, die Schaltung für die Ausführung auf Hardware zu optimieren, indem die Tiefe im Vergleich zu einer rein unitären Implementierung verringert wird. Zur Veranschaulichung der Vorteile werden wir sowohl die dynamische LRCX-Konstruktion als auch ihr unitäres Äquivalent darstellen und später ihre Leistung nach der Transpilierung vergleichen. Wichtig ist, dass wir hier das LRCX auf ein einfaches Hadamard-initialisiertes Problem anwenden, aber das Protokoll kann auf jede Schaltung angewendet werden, für die ein CNOT mit großer Reichweite erforderlich ist.

Glockenpaare vorbereiten

Wir beginnen damit, eine Kette von Bell-Paaren entlang des Pfades zwischen dem Kontroll- und dem Ziel-Qubit zu erstellen. Wenn der Abstand ungerade ist, wird zunächst ein CNOT von der Kontrolle zu ihrem Nachbarn angewendet, der der CNOT ist, der teleportiert werden soll. Bei einem geraden Abstand wird dieser CNOT nach dem Schritt der Vorbereitung des Glockenpaars angewendet. Die Bell-Paar-Kette verschränkt dann aufeinanderfolgende Qubit-Paare und stellt so die Ressource bereit, die für die Übertragung der Steuerinformationen über das Gerät erforderlich ist.

# 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)

Output:

Output of the previous code cell

Messe benachbarte Qubit-Paare in der Bell-Basis

Als nächstes messen wir unverschrä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 zur Kontrolle benachbarten Qubit (bis zu Pauli-Korrekturen, die im nächsten Schritt per Feedforward implementiert werden). Parallel dazu implementieren wir die Verschränkungsmessung, die das CNOT-Gate auf das beabsichtigte 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)

Output:

Output of the previous code cell

Wende Feedforward-Korrekturen an, um die Pauli-Nebenproduktoperatoren zu korrigieren

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

Mit dem neuen Framework für klassische Ausdrücke in Qiskit können diese Paritäten direkt in der klassischen Verarbeitungsschicht des Schaltkreises berechnet werden. Anstatt für jedes Messbit eine Folge von einzelnen bedingten Gattern anzuwenden, können wir einen einzigen klassischen Ausdruck bilden, der das XOR (Parität) aller relevanten Messergebnisse darstellt. Dieser Ausdruck wird dann als Bedingung in einem einzigen if_test -Block verwendet, so dass die Korrekturgatter in konstanter Tiefe angewendet werden können. Dieser Ansatz vereinfacht die Schaltung und stellt sicher, dass die Vorwärtskorrekturen keine unnötige zusätzliche Latenzzeit verursachen.

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)

Output:

Output of the previous code cell

Mess- und Ziel-Qubits

Wir definieren eine Hilfsfunktion, die die Messung der Kontroll- und Zielqubits in den Basen XXXX, YYYY oder ZZZZ ermöglicht. Um den Bell-Zustand (∣00⟩+∣11⟩)/2(|00\rangle + |11\rangle)/\sqrt{2} zu überprüfen, sollten die Erwartungswerte von XXXX und ZZZZ beide +1+1 sein, da sie Stabilisatoren des Zustands sind. Die Messung YYYY wird auch hier unterstützt und wird im Folgenden zur Berechnung der Wiedergabetreue 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")
qc_YY.draw(
    output="mpl", fold=-1, scale=0.5
)  # Circuit for measuring in the YY basis

Output:

Output of the previous code cell

Alles zusammenfügen

Wir kombinieren die oben definierten Schritte, um an beiden Enden einer eindimensionalen ( 1D ) Linie ein Langreichweiten-CX-Gate zu erzeugen. Die Schritte umfassen Folgendes:

  • Initialisierung des Steuerqubits in „ ∣+⟩|+\rangle “
  • Vorbereiten von Glockenpaaren
  • Messung benachbarter Qubit-Paare
  • Anwendung von Feedforward-Korrekturen in Abhängigkeit 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"]
]

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

Output:

Output of the previous code cell

Einheitliche Implementierung, bei der die Qubits in die Mitte verschoben werden

Zum Vergleich betrachten wir zunächst den Fall, in dem ein CNOT-Gatter mit großer Reichweite mithilfe von Verbindungen zwischen den nächsten Nachbarn und unitären Gattern realisiert wird. In der folgenden Abbildung ist links eine Schaltung für ein CNOT-Gatter mit großer Reichweite dargestellt, das eine „ 1D “-Kette aus n-Qubits überspannt, bei der ausschließlich Verbindungen zwischen den nächsten Nachbarn bestehen. In der Mitte steht eine äquivalente unitäre Zerlegung, die mit lokalen CNOT-Gattern implementierbar ist, Schaltungstiefe O(n)O(n).

CNOT-Schaltung mit großer Reichweite

Die Schaltung in der Mitte lässt sich wie folgt realisieren:

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


qc_uni = cnot_unitary(distance)

Bauen Sie nun die Schaltungen auf, die im Basisbereich von XXXX, YYYY und ZZZZ messen, genau wie wir es oben bei den dynamischen Schaltungen getan haben.

# Apply the measurement in the XX, YY, and ZZ bases
qc_uni_XX, qc_uni_YY, qc_uni_ZZ = [
    measure_in_basis(qc_uni, basis=basis) for basis in ["XX", "YY", "ZZ"]
]

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

Output:

Output of the previous code cell

distance=6Nachdem wir nun sowohl die dynamischen als auch die unitären Schaltkreise für ein kleines Beispiel mit erstellt haben, transpilieren wir sie, um sie zunächst auf einem rauschfreien Simulator auszuführen.

from qiskit_aer import AerSimulator

aer_backend = AerSimulator()
pm_sim = generate_preset_pass_manager(
    optimization_level=0, backend=aer_backend
)

# Dynamic circuits
isa_sim_dyn = pm_sim.run([qc_XX, qc_YY, qc_ZZ])

# Unitary circuits
isa_sim_uni = pm_sim.run([qc_uni_XX, qc_uni_YY, qc_uni_ZZ])

Schritt 3: Mit „ Qiskit primitives “ ausführen

Wir können das Experiment nun auf dem rauschfreien Simulator-Backend ausführen. Wir verwenden den „ Qiskit Runtime “-Sampler mit dem Modus „ AerSimulator “ als Backend, um die Schaltungen auszuführen.

sampler_sim = Sampler(mode=aer_backend)
sim_job = sampler_sim.run(isa_sim_dyn + isa_sim_uni)
sim_results = sim_job.result()

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

Nachdem die Experimente erfolgreich durchgeführt wurden, werten wir nun die Messwerte aus, um aussagekräftige Kennzahlen zu ermitteln. In diesem Schritt gehen wir wie folgt vor:

  • Festlegung von Qualitätsmaßstäben für die Bewertung der Leistung der weitreichenden CX.
  • Berechnung von Erwartungswerten von Pauli-Operatoren aus rohen Messergebnissen.
  • Verwenden Sie diese, um die Treue des erzeugten Bell-Zustands zu berechnen.

In einer geräuschlosen Simulation werden wir überprüfen, ob die Genauigkeitsmetrik für die konstruierten Schaltungen den Wert 11 annimmt. In den Experimenten mit den realen QPUs wird diese Analyse ein klares Bild davon liefern, wie gut die dynamischen Schaltungen im Vergleich zur unitären Basisimplementierung abschneiden.

Qualitätsmetriken

Um den Erfolg des Langreichweiten-CX-Protokolls zu bewerten, messen wir, wie nah der Ausgangszustand am idealen Bell-Zustand liegt. Eine bequeme Möglichkeit, dies zu quantifizieren, besteht darin, die Zustandsgenauigkeit anhand der Erwartungswerte der Pauli-Operatoren zu berechnen. Wir können die Treue für einen Bell-Zustand in Bezug auf den Steuer- und den Zielzustand berechnen, sobald uns die Werte von ⟨XX⟩\braket{XX}, ⟨YY⟩\braket{YY} und ⟨ZZ⟩\braket{ZZ} bekannt sind. Insbesondere gilt:

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

Um diese Erwartungswerte aus den Rohmessdaten zu berechnen, definieren wir eine Reihe von Hilfsfunktionen:

  • compute_ZZ_expectation: Berechnet bei gegebenen Messwerten den Erwartungswert eines Zwei-Qubit-Pauli-Operators auf der Basis ZZ.
  • compute_fidelity: Kombiniert die Erwartungswerte von XXXX, YYYY und ZZZZ in den obigen Treueausdruck.
  • get_counts_from_bitarray: Dienstprogramm 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)
# Dynamic fidelity
counts_xx = sim_results[0].data.cr.get_counts()
counts_yy = sim_results[1].data.cr.get_counts()
counts_zz = sim_results[2].data.cr.get_counts()
fidelity_dyn = compute_fidelity(counts_xx, counts_yy, counts_zz)

# Unitary fidelity
counts_xx = sim_results[3].data.cr.get_counts()
counts_yy = sim_results[4].data.cr.get_counts()
counts_zz = sim_results[5].data.cr.get_counts()
fidelity_uni = compute_fidelity(counts_xx, counts_yy, counts_zz)

print(f"Dynamic fidelity (distance={distance}): {fidelity_dyn:.4f}")
print(f"Unitary fidelity (distance={distance}): {fidelity_uni:.4f}")

Output:

Dynamic fidelity (distance=6): 1.0000
Unitary fidelity (distance=6): 1.0000

Wie bei einer rauschfreien Simulation zu erwarten, betragen die Genauigkeiten sowohl in dynamischen als auch in unitären Schaltungen 11.


Beispiel für groß angelegte Hardware

Hier führen wir nun all diese Details zu einem einzigen, groß angelegten Arbeitsablauf zusammen, der anschließend auf echter Quantenhardware ausgeführt wird.

Schaltungen für verschiedene Entfernungen generieren

Wir generieren nun CX-Schaltungen mit großer Reichweite für eine Reihe von Qubit-Abständen von bis zu 60 Qubits. Für jede Distanz erstellen wir Kreise, die in den Basen XXXX, YYYY und ZZZZ gemessen werden und später zur Berechnung der Treuewerte herangezogen werden.

Die Liste der Entfernungen umfasst sowohl Kurz- als auch Langstreckenabstände, wobei distance = 0 einem CX-Nachbarn entspricht. Dieselben Abstände werden später zum Vergleich auch für die Erstellung der entsprechenden Einheitsschaltungen verwendet.

# -------------------------Step 1-------------------------
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
basis_list = ["XX", "YY", "ZZ"]

# Dynamic circuits
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)}")

# Unitary circuits
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)}")

Nun, da wir sowohl dynamische als auch unitäre Schaltungen für eine Reihe von Entfernungen haben, sind wir bereit für die Transpilation. Zunächst müssen wir ein Backend-Gerät auswählen.

# -------------------------Step 2-------------------------
# 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 Anweisung if_else unterstützt, die für die neuere Version der dynamischen Schaltungen erforderlich ist. Da sich diese Funktion noch im Anfangsstadium befindet, fügen wir die IfElseOp explizit zum Backend-Ziel hinzu, wenn sie nicht bereits verfügbar ist.

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

Verwenden Sie die Zeichenfolge „Layer Fidelity“, um die „ 1D “-Kette auszuwählen

Da wir die Leistung von dynamischen und unitären Schaltungen auf einer 1D Kette vergleichen wollen, verwenden wir den Layer Fidelity String, um eine lineare Topologie der besten Kette von Qubits aus dem Gerät auszuwählen. Dadurch wird sichergestellt, dass beide Arten von Schaltungen unter denselben Konnektivitätsbedingungen umgesetzt werden, was einen fairen Vergleich ihrer Leistung ermöglicht.

# 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

Output:

[11, 12, 13, 14, 15, 19, 35, 34, 33, 39, 53, 54, 55, 59, 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 64, 63, 62, 61, 76, 81, 82, 83, 84, 85, 86, 87, 97, 107, 108, 109, 110, 111, 98, 91, 92, 93, 94, 95, 99, 115, 114, 113, 119, 133, 132, 131, 138, 151, 150, 149, 148]
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: "
    f"{isa_circuits_dyn[14].depth(lambda x: x.operation.num_qubits == 2)}"
)
isa_circuits_dyn[14].draw("mpl", fold=-1, idle_wires=0)

Output:

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

Output:

2Q depth: 13
Output of the previous code cell

Visualisieren Sie die für den LRCX-Schaltkreis verwendeten Qubits

In diesem Abschnitt wird untersucht, wie die LRCX-Schaltung auf die Hardware abgebildet wird. Wir beginnen mit der Visualisierung der physikalischen Qubits, die in der Schaltung verwendet werden, und untersuchen dann, wie sich der Abstand zwischen Kontrolle und Ziel im Layout auf die Anzahl der Operationen auswirkt.

# 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(),
)

Output:

Output of the previous code cell

Als Nächstes führen wir das Experiment auf dem eigentlichen Backend durch. Außerdem nutzen wir Batching, um das Experiment über mehrere Durchläufe hinweg effizient durchzuführen. Durch die Durchführung wiederholter Versuche können wir Durchschnittswerte berechnen, um einen genaueren Vergleich zwischen der unitären und der dynamischen Methode anzustellen, und zudem deren Variabilität quantifizieren, indem wir die Abweichungen zwischen den einzelnen Durchläufen vergleichen.

# -------------------------Step 3-------------------------
num_trials = 10
jobs_uni = []
jobs_dyn = []
with Batch(backend=backend) as batch:
    sampler = Sampler(mode=batch)
    sampler.options.environment.job_tags = ["TUT_LRE"]
    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))

Wir berechnen die Genauigkeit für die dynamischen CX-Schaltungen mit großer Reichweite. Für jede Distanz extrahieren wir die Messergebnisse aus den Datenbanken ⟨XX⟩\braket{XX}, ⟨YY⟩\braket{YY} und ⟨ZZ⟩\braket{ZZ}. Diese Ergebnisse werden mithilfe der zuvor definierten Hilfsfunktionen kombiniert, um die Treue gemäß F=14(1+⟨XX⟩−⟨YY⟩+⟨ZZ⟩)F = \tfrac{1}{4} \big( 1 + \langle XX \rangle - \langle YY \rangle + \langle ZZ \rangle \big) zu berechnen. Dies liefert die beobachtete Treue des dynamisch ausgeführten Protokolls bei jedem Abstand.

# -------------------------Step 4-------------------------
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)

Jetzt berechnen wir die Treue für die unitäre CX-Schaltung mit großer Reichweite, und zwar auf dieselbe Weise wie für die dynamischen Schaltungen 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)

Zeichnen Sie die Ergebnisse auf

Um die Ergebnisse visuell zu veranschaulichen, sind in der folgenden Grafik die geschätzten Gate-Treffgenauigkeiten dargestellt, die bei unterschiedlichen Abständen zwischen verschränkten Qubits für jede Methode gemessen wurden.

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()

Output:

Output of the previous code cell

Aus dem obigen Diagramm geht hervor, dass LRCX nicht durchgängig besser abschneidet als die direkte unitäre Implementierung. Bei kurzen Abständen zwischen Steuerung und Ziel erreicht die unitäre Schaltung sogar eine höhere Genauigkeit. Bei größeren Abständen beginnt die dynamische Schaltung jedoch, eine bessere Genauigkeit zu erreichen als die unitäre Implementierung. Dieses Verhalten ist bei aktueller Hardware nicht unerwartet: Dynamische Schaltungen verringern zwar die Schaltungstiefe, indem sie lange SWAP-Ketten vermeiden, führen aber durch Messungen in der Mitte der Schaltung, klassische Feedforward- und Steuerpfad-Verzögerungen zu zusätzlicher Schaltungszeit. Die zusätzliche Latenzzeit erhöht die Dekohärenz und die Auslesefehler, was bei kurzen Entfernungen die Tiefeneinsparungen aufwiegen kann.

Dennoch beobachten wir einen Übergangspunkt, an dem der dynamische Ansatz den einheitlichen Ansatz übertrifft. Dies ist eine direkte Folge der unterschiedlichen Skalierung: die Tiefe der unitären Schaltung wächst linear mit dem Abstand zwischen den Qubits, während die Tiefe der dynamischen Schaltung konstant bleibt.

Wichtigste Punkte:

  • Unmittelbarer Nutzen von dynamischen Schaltungen: Die Hauptmotivation ist heute die Verringerung der Zwei-Qubit-Tiefe, nicht unbedingt die Verbesserung der Wiedergabetreue.
  • Warum die Genauigkeit heute schlechter sein kann: Erhöhte Schaltzeiten durch Messungen und klassische Operationen dominieren oft, insbesondere wenn der Abstand zwischen Steuerung und Ziel klein ist.
  • Ein Blick in die Zukunft: Mit der Verbesserung der Hardware, insbesondere durch schnelleres Auslesen, kürzere Latenzzeiten bei der klassischen Steuerung und geringeren Overhead in der Mitte des Schaltkreises, sollten wir erwarten, dass sich diese Verringerung der Tiefe und Dauer in messbarem Zuwachs an Wiedergabetreue niederschlägt.
# 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()

Output:

Output of the previous code cell

Diese Darstellung der Zwei-Qubit-Tiefe verdeutlicht den Hauptvorteil des mit dynamischen Schaltungen implementierten LRCX: Die Leistung bleibt im Wesentlichen konstant, wenn der Abstand zwischen Kontroll- und Ziel-Qubit zunimmt. Im Gegensatz dazu wächst die unitäre Implementierung aufgrund der erforderlichen SWAP-Ketten linear mit der Entfernung. Die Tiefe erfasst die logische Skalierung von Zwei-Qubit-Operationen, während die Anzahl der Messungen den zusätzlichen Overhead für dynamische Schaltungen widerspiegelt. Diese Messungen sind effizient, da sie parallel durchgeführt werden, aber sie verursachen auf der heutigen Hardware immer noch feste Kosten.

Warum die Genauigkeit heute schlechter sein kann: Erhöhte Schaltzeiten durch Messungen und klassische Operationen dominieren oft, insbesondere wenn der Abstand zwischen Steuerung und Ziel klein ist. So beträgt beispielsweise die durchschnittliche Ausleselänge eines Heron r2 Prozessors 2.280 ns, während seine 2Q Gatterlänge nur 68 ns beträgt.

Da sich die Mess- und klassischen Latenzzeiten verbessern, erwarten wir, dass die Skalierung dynamischer Schaltungen mit konstanter Tiefe und konstanter Messung bei größeren Schaltungen deutliche Vorteile in Bezug auf Genauigkeit und Laufzeit bringt.


Nächste Schritte

Wenn Sie diese Arbeit interessant fanden, könnten Sie auch an folgenden Materialien interessiert sein:


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

War diese Seite hilfreich?
Melden Sie auf GitHub einen Fehler, einen Tippfehler oder fordern Sie Inhalte an.