Circuit Cutting zur Tiefenreduzierung
Geschätzter Aufwand: Acht Minuten auf einem Eagle-Prozessor (HINWEIS: Dies ist nur eine Schätzung. Deine Laufzeit kann abweichen.)
Hintergrund
Dieses Tutorial zeigt, wie du ein Qiskit pattern zum Schneiden von Gates in einem Quantenschaltkreis aufbaust, um die Schaltkreistiefe zu reduzieren. Eine ausführlichere Diskussion zum Circuit Cutting findest du in den Docs zum Circuit-Cutting-Qiskit-Addon.
Voraussetzungen
Stelle vor Beginn dieses Tutorials sicher, dass Folgendes installiert ist:
- Qiskit SDK v2.0 oder höher, mit Unterstützung für Visualisierung
- Qiskit Runtime v0.22 oder höher (
pip install qiskit-ibm-runtime) - Circuit-Cutting-Qiskit-Addon v0.9.0 oder höher (
pip install qiskit-addon-cutting)
Setup
# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-addon-cutting qiskit-ibm-runtime
import numpy as np
from qiskit.circuit.library import EfficientSU2
from qiskit.quantum_info import PauliList, Statevector, SparsePauliOp
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_addon_cutting import (
cut_gates,
generate_cutting_experiments,
reconstruct_expectation_values,
)
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2
Schritt 1: Klassische Eingaben auf ein Quantenproblem abbilden
Wir implementieren unser Qiskit-Pattern anhand der vier Schritte, die in den Docs beschrieben sind. In diesem Fall simulieren wir Erwartungswerte eines Schaltkreises einer bestimmten Tiefe, indem wir Gates schneiden, die dabei entstehenden Swap-Gates ersetzen und Teilexperimente auf flacheren Schaltkreisen ausführen. Gate-Cutting ist relevant für Schritt 2 (Schaltkreis für die Quantenausführung optimieren, indem weit entfernte Gates zerlegt werden) und Schritt 4 (Nachverarbeitung zur Rekonstruktion der Erwartungswerte des ursprünglichen Schaltkreises). Im ersten Schritt erzeugen wir einen Schaltkreis aus der Qiskit-Schaltkreisbibliothek und definieren einige Observablen.
- Eingabe: Klassische Parameter zur Definition eines Schaltkreises
- Ausgabe: Abstrakter Schaltkreis und Observablen
circuit = EfficientSU2(num_qubits=4, entanglement="circular").decompose()
circuit.assign_parameters([0.4] * len(circuit.parameters), inplace=True)
observables = PauliList(["ZZII", "IZZI", "IIZZ", "XIXI", "ZIZZ", "IXIX"])
circuit.draw("mpl", scale=0.8, style="iqp")
Schritt 2: Problem für die Ausführung auf Quantenhardware optimieren
- Eingabe: Abstrakter Schaltkreis und Observablen
- Ausgabe: Zielschaltkreis und Observablen, erzeugt durch das Schneiden weit entfernter Gates zur Reduzierung der transpierten Schaltkreistiefe
Wir wählen ein initiales Layout, das zwei Swaps erfordert, um die Gates zwischen Qubits 3 und 0 auszuführen, sowie zwei weitere Swaps, um die Qubits in ihre Ausgangspositionen zurückzubringen. Wir wählen optimization_level=3, das höchste verfügbare Optimierungsniveau eines voreingestellten Pass-Managers.
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, min_num_qubits=circuit.num_qubits, simulator=False
)
pm = generate_preset_pass_manager(
optimization_level=3, initial_layout=[0, 1, 2, 3], backend=backend
)
transpiled_qc = pm.run(circuit)

print(f"Transpiled circuit depth: {transpiled_qc.depth()}")
transpiled_qc.draw("mpl", scale=0.4, idle_wires=False, style="iqp", fold=-1)
Transpiled circuit depth: 103
Weit entfernte Gates finden und schneiden: Wir ersetzen die weit entfernten Gates (Gates, die nicht-lokale Qubits 0 und 3 verbinden) durch TwoQubitQPDGate-Objekte, indem wir deren Indizes angeben. cut_gates ersetzt die Gates an den angegebenen Indizes durch TwoQubitQPDGate-Objekte und gibt außerdem eine Liste von QPDBasis-Instanzen zurück – eine für jede Gate-Zerlegung. Das QPDBasis-Objekt enthält Informationen darüber, wie die geschnittenen Gates in Einzelqubit-Operationen zerlegt werden.
# Find the indices of the distant gates
cut_indices = [
i
for i, instruction in enumerate(circuit.data)
if {circuit.find_bit(q)[0] for q in instruction.qubits} == {0, 3}
]
# Decompose distant CNOTs into TwoQubitQPDGate instances
qpd_circuit, bases = cut_gates(circuit, cut_indices)
qpd_circuit.draw("mpl", scale=0.8)
Die Teilexperimente für die Ausführung auf dem Backend erzeugen: generate_cutting_experiments akzeptiert einen Schaltkreis mit TwoQubitQPDGate-Instanzen sowie Observablen als PauliList.
Um den Erwartungswert des vollständigen Schaltkreises zu simulieren, werden viele Teilexperimente aus der gemeinsamen Quasiwahrscheinlichkeitsverteilung der zerlegten Gates erzeugt und dann auf einem oder mehreren Backends ausgeführt. Die Anzahl der aus der Verteilung gezogenen Stichproben wird durch num_samples gesteuert, wobei ein kombinierter Koeffizient für jede eindeutige Stichprobe angegeben wird. Weitere Informationen zur Berechnung der Koeffizienten findest du im Erklärungsmaterial.
# Generate the subexperiments and sampling coefficients
subexperiments, coefficients = generate_cutting_experiments(
circuits=qpd_circuit, observables=observables, num_samples=np.inf
)
Zum Vergleich: Die QPD-Teilexperimente sind nach dem Schneiden weit entfernter Gates flacher: Hier ist ein Beispiel eines beliebig gewählten Teilexperiments, das aus dem QPD-Schaltkreis erzeugt wurde. Seine Tiefe wurde um mehr als die Hälfte reduziert. Viele dieser probabilistischen Teilexperimente müssen erzeugt und ausgewertet werden, um einen Erwartungswert des tieferen Schaltkreises zu rekonstruieren.
# Transpile the decomposed circuit to the same layout
transpiled_qpd_circuit = pm.run(subexperiments[100])
print(f"Original circuit depth after transpile: {transpiled_qc.depth()}")
print(
f"QPD subexperiment depth after transpile: {transpiled_qpd_circuit.depth()}"
)
transpiled_qpd_circuit.draw(
"mpl", scale=0.6, style="iqp", idle_wires=False, fold=-1
)
Original circuit depth after transpile: 103
QPD subexperiment depth after transpile: 46
Andererseits erfordert das Schneiden zusätzliches Sampling. Hier schneiden wir drei CNOT-Gates, was zu einem Sampling-Overhead von führt. Mehr zum Sampling-Overhead beim Circuit Cutting findest du in der Dokumentation des Circuit Knitting Toolbox.
print(f"Sampling overhead: {np.prod([basis.overhead for basis in bases])}")
Sampling overhead: 729.0
Schritt 3: Mit Qiskit-Primitiven ausführen
Führe die Zielschaltkreise ("Teilexperimente") mit dem Sampler-Primitiv aus.
- Eingabe: Zielschaltkreise
- Ausgabe: Quasi-Wahrscheinlichkeitsverteilungen
# Transpile the subexperiments to the backend's instruction set architecture (ISA)
isa_subexperiments = pm.run(subexperiments)
# Set up the Qiskit Runtime Sampler primitive. For a fake backend, this will use a local simulator.
sampler = SamplerV2(backend)
# Submit the subexperiments
job = sampler.run(isa_subexperiments)
# Retrieve the results
results = job.result()
print(job.job_id())
czypg1r6rr3g008mgp6g
Schritt 4: Nachverarbeitung und Rückgabe des Ergebnisses im gewünschten klassischen Format
Verwende die Teilexperiment-Ergebnisse, Teil-Observablen und Sampling-Koeffizienten, um den Erwartungswert des ursprünglichen Schaltkreises zu rekonstruieren.
Eingabe: Quasi-Wahrscheinlichkeitsverteilungen Ausgabe: Rekonstruierte Erwartungswerte
reconstructed_expvals = reconstruct_expectation_values(
results,
coefficients,
observables,
)
# Reconstruct final expectation value
final_expval = np.dot(reconstructed_expvals, [1] * len(observables))
print("Final reconstructed expectation value")
print(final_expval)
Final reconstructed expectation value
1.0751342773437473
ideal_expvals = [
Statevector(circuit).expectation_value(SparsePauliOp(observable))
for observable in observables
]
print("Ideal expectation value")
print(np.dot(ideal_expvals, [1] * len(observables)).real)
Ideal expectation value
1.2283177520039992
Tutorial-Umfrage
Bitte nimm an dieser kurzen Umfrage teil, um Feedback zu diesem Tutorial zu geben. Deine Rückmeldungen helfen uns, unsere Inhalte und die Nutzererfahrung zu verbessern.