Gate Cutting zur Reduzierung der Circuit-Tiefe
In diesem Tutorial werden wir die Tiefe eines Circuits reduzieren, indem wir entfernte Gates schneiden und so die SWAP-Gates vermeiden, die andernfalls durch das Routing eingeführt würden.
Dies sind die Schritte, die wir in diesem Qiskit-Muster durchführen werden:
- Schritt 1: Problem auf Quantum Circuits und Operatoren abbilden:
- Den Hamiltonian auf einen Quantum Circuit abbilden.
- Schritt 2: Für die Zielhardware optimieren [Verwendet das Cutting-Addon]:
- Den Circuit und das Observable schneiden.
- Die Teilexperimente für die Hardware transpilieren.
- Schritt 3: Auf der Zielhardware ausführen:
- Die in Schritt 2 erhaltenen Teilexperimente mit einem
Sampler-Primitiv ausführen.
- Die in Schritt 2 erhaltenen Teilexperimente mit einem
- Schritt 4: Ergebnisse nachverarbeiten [Verwendet das Cutting-Addon]:
- Die Ergebnisse aus Schritt 3 kombinieren, um den Erwartungswert des betreffenden Observables zu rekonstruieren.
Schritt 1: Abbilden
Einen Circuit erstellen, der auf dem Backend ausgeführt wird
# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-addon-cutting qiskit-aer qiskit-ibm-runtime
from qiskit.circuit.library import efficient_su2
circuit = efficient_su2(num_qubits=4, entanglement="circular")
circuit.assign_parameters([0.4] * len(circuit.parameters), inplace=True)
circuit.draw("mpl", scale=0.8)

Ein Observable festlegen
from qiskit.quantum_info import SparsePauliOp
observable = SparsePauliOp(["ZZII", "IZZI", "-IIZZ", "XIXI", "ZIZZ", "IXIX"])
Schritt 2: Optimieren
Ein Backend festlegen
Du kannst entweder ein Fake-Backend oder ein Hardware-Backend von Qiskit Runtime angeben.
from qiskit_ibm_runtime.fake_provider import FakeManilaV2
backend = FakeManilaV2()
Den Circuit transpilieren, die Swaps visualisieren und die Tiefe notieren
Wir wählen ein Layout, das zwei Swaps erfordert, um die Gates zwischen Qubits 3 und 0 auszuführen, und weitere zwei Swaps, um die Qubits in ihre Ausgangspositionen zurückzubringen.
from qiskit.transpiler import generate_preset_pass_manager
pass_manager = generate_preset_pass_manager(
optimization_level=1, backend=backend, initial_layout=[0, 1, 2, 3]
)
transpiled_qc = pass_manager.run(circuit)
print(f"Transpiled circuit depth: {transpiled_qc.depth(lambda x: len(x.qubits) >= 2)}")
Transpiled circuit depth: 30
transpiled_qc.draw("mpl", scale=0.4, idle_wires=False, fold=-1)

Entfernte Gates durch TwoQubitQPDGates ersetzen, indem ihre Indizes angegeben werden
cut_gates ersetzt die Gates an den angegebenen Indizes durch TwoQubitQPDGates und gibt außerdem eine Liste von QPDBasis-Instanzen zurück – eine für jede Gate-Dekomposition.
from qiskit_addon_cutting import cut_gates
# 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 generieren, die auf dem Backend ausgeführt werden sollen
generate_cutting_experiments akzeptiert einen Circuit, der TwoQubitQPDGate-Instanzen enthält, und Observables als PauliList.
Um den Erwartungswert des vollständigen Circuits zu simulieren, werden viele Teilexperimente aus der gemeinsamen Quasiwahrscheinlichkeitsverteilung der zerlegten Gates generiert und anschließend auf einem oder mehreren Backends ausgeführt. Die Anzahl der aus der Verteilung entnommenen Stichproben wird durch num_samples gesteuert, und für jede eindeutige Stichprobe wird ein kombinierter Koeffizient angegeben. Weitere Informationen zur Berechnung der Koeffizienten findest du im Erläuterungsmaterial.
Hinweis: Das observables-Schlüsselwortargument für generate_cutting_experiments hat den Typ PauliList. Koeffizienten und Phasen der Observable-Terme werden bei der Zerlegung des Problems und der Ausführung der Teilexperimente ignoriert. Sie können bei der Rekonstruktion des Erwartungswertes erneut angewendet werden.
import numpy as np
from qiskit_addon_cutting import generate_cutting_experiments
# Generate the subexperiments and sampling coefficients
subexperiments, coefficients = generate_cutting_experiments(
circuits=qpd_circuit, observables=observable.paulis, num_samples=np.inf
)
Den Sampling-Overhead für die gewählten Schnitte berechnen
Hier schneiden wir drei CNOT-Gates, was zu einem Sampling-Overhead von führt.
Weitere Informationen zum durch Circuit Cutting entstehenden Sampling-Overhead findest du im Erläuterungsmaterial.
print(f"Sampling overhead: {np.prod([basis.overhead for basis in bases])}")
Sampling overhead: 729.0
Demonstrieren, dass die QPD-Teilexperimente nach dem Schneiden entfernter Gates flacher sein werden
Hier ist ein Beispiel eines beliebig gewählten Teilexperiments, das aus dem QPD-Circuit generiert wurde. Seine Tiefe wurde um mehr als die Hälfte reduziert. Viele dieser probabilistischen Teilexperimente müssen generiert und ausgewertet werden, um einen Erwartungswert des tieferen Circuits zu rekonstruieren.
# Transpile the decomposed circuit to the same layout
transpiled_qpd_circuit = pass_manager.run(subexperiments[100])
print(
f"Original circuit depth after transpile: {transpiled_qc.depth(lambda x: len(x.qubits) >= 2)}"
)
print(
f"QPD subexperiment depth after transpile: {transpiled_qpd_circuit.depth(lambda x: len(x.qubits) >= 2)}"
)
transpiled_qpd_circuit.draw("mpl", scale=0.8, idle_wires=False, fold=-1)
Original circuit depth after transpile: 30
QPD subexperiment depth after transpile: 7

Teilexperimente für das Backend vorbereiten
# Transpile the subeperiments to the backend's instruction set architecture (ISA)
isa_subexperiments = pass_manager.run(subexperiments)
Schritt 3: Ausführen
Die Teilexperimente mit dem Qiskit Runtime Sampler-Primitiv ausführen
from qiskit_ibm_runtime import SamplerV2
# 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()
Schritt 4: Nachverarbeitung
Den Erwartungswert rekonstruieren
Erwartungswerte für jeden Observable-Term rekonstruieren und kombinieren, um den Erwartungswert für das ursprüngliche Observable zu rekonstruieren.
from qiskit_addon_cutting import reconstruct_expectation_values
reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
observable.paulis,
)
# Reconstruct final expectation value
reconstructed_expval = np.dot(reconstructed_expval_terms, observable.coeffs)
Den rekonstruierten Erwartungswert mit dem exakten Erwartungswert aus dem ursprünglichen Circuit und Observable vergleichen
from qiskit_aer.primitives import EstimatorV2
estimator = EstimatorV2()
exact_expval = estimator.run([(circuit, observable)]).result()[0].data.evs
print(f"Reconstructed expectation value: {np.real(np.round(reconstructed_expval, 8))}")
print(f"Exact expectation value: {np.round(exact_expval, 8)}")
print(f"Error in estimation: {np.real(np.round(reconstructed_expval-exact_expval, 8))}")
print(
f"Relative error in estimation: {np.real(np.round((reconstructed_expval-exact_expval) / exact_expval, 8))}"
)
Reconstructed expectation value: 0.44018555
Exact expectation value: 0.50497603
Error in estimation: -0.06479049
Relative error in estimation: -0.12830408