Einstieg in Circuit Cutting mit Gate Cuts
Paketversionen
Der Code auf dieser Seite wurde mit den folgenden Anforderungen entwickelt. Wir empfehlen, diese Versionen oder neuere zu verwenden.
qiskit[all]~=2.3.0
qiskit-ibm-runtime~=0.43.1
qiskit-aer~=0.17
qiskit-addon-cutting~=0.10.0
Diese Anleitung zeigt zwei funktionsfähige Beispiele für Gate Cuts mit dem qiskit-addon-cutting-Paket. Das erste Beispiel zeigt, wie du die Circuit-Tiefe (die Anzahl der Circuit-Anweisungen) reduzierst, indem du verschränkende Gates auf nicht benachbarten Qubits schneidest, die beim Transpilieren auf Hardware ansonsten einen SWAP-Overhead verursachen würden. Das zweite Beispiel zeigt, wie du Gate Cutting verwendest, um die Circuit-Breite (die Anzahl der Qubits) zu reduzieren, indem du einen Circuit in mehrere Circuits mit weniger Qubits aufteilst.
Beide Beispiele verwenden den efficient_su2-Ansatz und rekonstruieren dasselbe Observable.
Gate Cutting zur Reduzierung der Circuit-Tiefe
Der folgende Workflow reduziert die Tiefe eines Circuits, indem er weit entfernte Gates schneidet und so eine große Anzahl von SWAP-Gates vermeidet, die andernfalls eingeführt würden.
Beginne mit dem efficient_su2-Ansatz mit „kreisförmiger" Verschränkung, um weit entfernte Gates einzuführen.
# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-addon-cutting qiskit-aer qiskit-ibm-runtime
import numpy as np
from qiskit.circuit.library import efficient_su2
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler import generate_preset_pass_manager
from qiskit_ibm_runtime.fake_provider import FakeManilaV2
from qiskit_ibm_runtime import SamplerV2, Batch
from qiskit_aer.primitives import EstimatorV2
from qiskit_addon_cutting import (
cut_gates,
partition_problem,
generate_cutting_experiments,
reconstruct_expectation_values,
)
circuit = efficient_su2(num_qubits=4, entanglement="circular")
circuit.assign_parameters([0.4] * len(circuit.parameters), inplace=True)
observable = SparsePauliOp(["ZZII", "IZZI", "-IIZZ", "XIXI", "ZIZZ", "IXIX"])
print(f"Observable: {observable}")
circuit.draw("mpl", scale=0.8)
Observable: SparsePauliOp(['ZZII', 'IZZI', 'IIZZ', 'XIXI', 'ZIZZ', 'IXIX'],
coeffs=[ 1.+0.j, 1.+0.j, -1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j])
Jedes der CNOT-Gates zwischen den Qubits und führt nach der Transpilierung zwei SWAP-Gates ein (vorausgesetzt, die Qubits sind in einer geraden Linie verbunden). Um diesen Tiefenzuwachs zu vermeiden, kannst du diese weit entfernten Gates durch TwoQubitQPDGate-Objekte ersetzen, indem du die Methode cut_gates() verwendest. Diese Funktion gibt außerdem eine Liste von QPDBasis-Instanzen zurück – eine für jede Zerlegung.
# 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)
Nachdem die geschnittenen Gate-Anweisungen hinzugefügt wurden, haben die Teilexperimente nach der Transpilierung eine geringere Tiefe als der ursprüngliche Circuit. Der folgende Code-Ausschnitt generiert die Teilexperimente mithilfe von generate_cutting_experiments, das den Circuit und das zu rekonstruierende Observable verarbeitet.
Das Argument num_samples gibt an, wie viele Samples aus der Quasi-Wahrscheinlichkeitsverteilung gezogen werden, und bestimmt die Genauigkeit der Koeffizienten, die für die Rekonstruktion verwendet werden. Die Übergabe von Unendlich (np.inf) stellt sicher, dass alle Koeffizienten exakt berechnet werden. Lies die API-Dokumentation zum Generieren von Gewichtungen und zum Generieren von Cutting-Experimenten für weitere Informationen.
Sobald die Teilexperimente generiert sind, kannst du sie transpilieren und das Sampler-Primitiv verwenden, um die Verteilung abzutasten und die geschätzten Erwartungswerte zu rekonstruieren. Der folgende Code-Block generiert, transpiliert und führt die Teilexperimente aus. Anschließend rekonstruiert er die Ergebnisse und vergleicht sie mit dem exakten Erwartungswert.
# Generate the subexperiments and sampling coefficients
subexperiments, coefficients = generate_cutting_experiments(
circuits=qpd_circuit, observables=observable.paulis, num_samples=np.inf
)
# Set a backend to use and transpile the subexperiments
backend = FakeManilaV2()
pass_manager = generate_preset_pass_manager(
optimization_level=1, backend=backend
)
isa_subexperiments = pass_manager.run(subexperiments)
# Set up the Qiskit Runtime Sampler primitive, submit the subexperiments, and retrieve the results
sampler = SamplerV2(backend)
job = sampler.run(isa_subexperiments, shots=4096 * 3)
results = job.result()
# Reconstruct the results
reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
observable.paulis,
)
# Apply the coefficients of the original observable
reconstructed_expval = np.dot(reconstructed_expval_terms, observable.coeffs)
estimator = EstimatorV2()
exact_expval = (
estimator.run([(circuit, observable, [0.4] * len(circuit.parameters))])
.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.49812826
Exact expectation value: 0.50497603
Error in estimation: -0.00684778
Relative error in estimation: -0.0135606
Um den Erwartungswert korrekt zu rekonstruieren, müssen die Koeffizienten des ursprünglichen Observables (die sich von den Koeffizienten in der Ausgabe von generate_cutting_experiments() unterscheiden) auf die Ausgabe der Rekonstruktion angewendet werden, da diese Information verloren geht, wenn die Cutting-Experimente generiert oder das Observable expandiert wird.
Diese Koeffizienten können typischerweise über numpy.dot() angewendet werden, wie oben gezeigt.
Gate Cutting zur Reduzierung der Circuit-Breite
Dieser Abschnitt zeigt, wie Gate Cutting verwendet wird, um die Circuit-Breite zu reduzieren. Beginne mit demselben efficient_su2, aber verwende „lineare" Verschränkung.
qc = efficient_su2(4, entanglement="linear", reps=2)
qc.assign_parameters([0.4] * len(qc.parameters), inplace=True)
observable = SparsePauliOp(["ZZII", "IZZI", "-IIZZ", "XIXI", "ZIZZ", "IXIX"])
print(f"Observable: {observable}")
qc.draw("mpl", scale=0.8)
Observable: SparsePauliOp(['ZZII', 'IZZI', 'IIZZ', 'XIXI', 'ZIZZ', 'IXIX'],
coeffs=[ 1.+0.j, 1.+0.j, -1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j])
Generiere dann die Teilcircuits und Teilobservables, die du mit der Funktion partition_problem() ausführen wirst. Diese Funktion nimmt den Circuit, das Observable und ein optionales Partitionierungsschema entgegen und gibt die geschnittenen Circuits und Observables in Form eines Wörterbuchs zurück.
Die Partitionierung wird durch einen Label-String der Form "AABB" definiert, wobei jedes Label in diesem String dem Qubit am selben Index des circuit-Arguments entspricht. Qubits mit einem gemeinsamen Partitions-Label werden zusammengefasst, und alle nicht-lokalen Gates, die mehr als eine Partition überspannen, werden geschnitten.
Das observables-Schlüsselwortargument für partition_problem hat den Typ PauliList. Koeffizienten und Phasen von Observable-Termen werden bei der Zerlegung des Problems und der Ausführung der Teilexperimente ignoriert. Sie können während der Rekonstruktion des Erwartungswertes erneut angewendet werden.
partitioned_problem = partition_problem(
circuit=qc, partition_labels="AABB", observables=observable.paulis
)
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables
bases = partitioned_problem.bases
print(f"Sampling overhead: {np.prod([basis.overhead for basis in bases])}")
print(f"Subobservables: {subobservables}")
subcircuits["A"].draw("mpl", scale=0.8)
Sampling overhead: 81.0
Subobservables: {'A': PauliList(['II', 'ZI', 'ZZ', 'XI', 'ZZ', 'IX']), 'B': PauliList(['ZZ', 'IZ', 'II', 'XI', 'ZI', 'IX'])}
subcircuits["B"].draw("mpl", scale=0.8)
Der nächste Schritt besteht darin, die Teilcircuits und Teilobservables zu verwenden, um mithilfe der Methode generate_cutting_experiments die auf einem QPU auszuführenden Teilexperimente zu generieren.
Um den Erwartungswert des vollständigen Circuits zu schätzen, werden viele Teilexperimente aus der gemeinsamen Quasi-Wahrscheinlichkeitsverteilung der zerlegten Gates generiert und dann auf einem oder mehreren QPUs ausgeführt. Die Anzahl der aus dieser Verteilung zu ziehenden Samples wird durch das Argument num_samples gesteuert.
Der folgende Code-Block generiert die Teilexperimente und führt sie mithilfe des Sampler-Primitivs auf einem lokalen Simulator aus. (Um sie auf einem QPU auszuführen, ändere den backend auf die gewünschte QPU-Ressource.)
subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits, observables=subobservables, num_samples=np.inf
)
# Set a backend to use and transpile the subexperiments
backend = FakeManilaV2()
pass_manager = generate_preset_pass_manager(
optimization_level=1, backend=backend
)
isa_subexperiments = {
label: pass_manager.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}
# Submit each partition's subexperiments to the Qiskit Runtime Sampler
# primitive, in a single batch so that the jobs will run back-to-back.
with Batch(backend=backend) as batch:
sampler = SamplerV2(mode=batch)
jobs = {
label: sampler.run(subsystem_subexpts, shots=4096 * 3)
for label, subsystem_subexpts in isa_subexperiments.items()
}
# Retrieve results
results = {label: job.result() for label, job in jobs.items()}
Abschließend wird der Erwartungswert des vollständigen Circuits mithilfe der Methode reconstruct_expectation_values rekonstruiert.
Der folgende Code-Block rekonstruiert die Ergebnisse und vergleicht sie mit dem exakten Erwartungswert.
# Get expectation values for each observable term
reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
# Reconstruct final expectation value
reconstructed_expval = np.dot(reconstructed_expval_terms, observable.coeffs)
estimator = EstimatorV2()
exact_expval = (
estimator.run([(qc, observable, [0.4] * len(qc.parameters))])
.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.53571896
Exact expectation value: 0.56254612
Error in estimation: -0.02682716
Relative error in estimation: -0.04768882
Nächste Schritte
- Lies die Anleitung Einstieg in Circuit Cutting mit Wire Cuts.
- Lies das arXiv-Paper zu Circuit Knitting mit klassischer Kommunikation.