Zum Hauptinhalt springen

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.
  • 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)

Quantum circuit diagram

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)

Quantum circuit diagram

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)

Quantum circuit diagram

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 939^3 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

Quantum circuit diagram

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