Einstieg in Circuit Cutting mit Wire Cuts
Paketversionen
Der Code auf dieser Seite wurde mit den folgenden Anforderungen entwickelt. Wir empfehlen, diese oder neuere Versionen 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 demonstriert ein funktionierendes Beispiel für Wire Cuts mit dem qiskit-addon-cutting-Paket. Sie zeigt, wie Erwartungswerte eines Sieben-Qubit-Circuits mithilfe von Wire Cutting rekonstruiert werden.
Ein Wire Cut wird in diesem Paket als zweiQubit-Move-Instruction dargestellt, die als Reset des zweiten Qubits, auf das die Instruction wirkt, gefolgt von einem Swap beider Qubits definiert ist. Diese Operation ist äquivalent dazu, den Zustand des ersten Qubits auf das zweite Qubit zu übertragen und gleichzeitig den eingehenden Zustand des zweiten Qubits zu verwerfen.
Das Paket ist so konzipiert, dass es konsistent mit der Art und Weise ist, wie Wire Cuts bei physikalischen Qubits behandelt werden müssen. Zum Beispiel kann ein Wire Cut den Zustand von physikalischem Qubit nehmen und ihn nach dem Schnitt als physikalisches Qubit fortführen. Du kannst dir „Instruction Cutting" als einheitliches Framework vorstellen, das Wire Cuts und Gate Cuts im selben Formalismus berücksichtigt (da ein Wire Cut einfach eine geschnittene Move-Instruction ist). Die Verwendung dieses Frameworks für Wire Cutting ermöglicht außerdem die Wiederverwendung von Qubits, was im Abschnitt über manuelles Schneiden von Wires erläutert wird.
Die einQubit-CutWire-Instruction dient als abstrahiertere, einfachere Schnittstelle für die Arbeit mit Wire Cuts. Sie ermöglicht es dir, auf hohem Niveau anzugeben, wo im Circuit ein Wire geschnitten werden soll, und das Circuit-Cutting-Addon die entsprechenden Move-Instructions automatisch einfügen zu lassen.
Das folgende Beispiel demonstriert die Rekonstruktion von Erwartungswerten nach Wire Cutting. Du wirst einen Circuit mit mehreren nicht-lokalen Gates erstellen und zu schätzende Observablen definieren.
# 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 import QuantumCircuit
from qiskit.transpiler import generate_preset_pass_manager
from qiskit.quantum_info import SparsePauliOp
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.instructions import Move, CutWire
from qiskit_addon_cutting import (
partition_problem,
generate_cutting_experiments,
cut_wires,
expand_observables,
reconstruct_expectation_values,
)
qc_0 = QuantumCircuit(7)
for i in range(7):
qc_0.rx(np.pi / 4, i)
qc_0.cx(0, 3)
qc_0.cx(1, 3)
qc_0.cx(2, 3)
qc_0.cx(3, 4)
qc_0.cx(3, 5)
qc_0.cx(3, 6)
qc_0.cx(0, 3)
qc_0.cx(1, 3)
qc_0.cx(2, 3)
# Define observable
observable = SparsePauliOp(["ZIIIIII", "IIIZIII", "IIIIIIZ"])
# Draw circuit
qc_0.draw("mpl")
Wires mit der High-Level-CutWire-Instruction schneiden
Als Nächstes führst du Wire Cuts mithilfe der einQubit-CutWire-Instruction auf Qubit durch. Sobald die Subexperimente zur Ausführung vorbereitet sind, wird die Funktion cut_wires() verwendet, um CutWire in Move-Instructions auf neu zugewiesenen Qubits umzuwandeln.
qc_1 = QuantumCircuit(7)
for i in range(7):
qc_1.rx(np.pi / 4, i)
qc_1.cx(0, 3)
qc_1.cx(1, 3)
qc_1.cx(2, 3)
qc_1.append(CutWire(), [3])
qc_1.cx(3, 4)
qc_1.cx(3, 5)
qc_1.cx(3, 6)
qc_1.append(CutWire(), [3])
qc_1.cx(0, 3)
qc_1.cx(1, 3)
qc_1.cx(2, 3)
qc_1.draw("mpl")
Wenn ein Circuit durch einen oder mehrere Wire Cuts erweitert wird, muss die Observable aktualisiert werden, um die zusätzlich eingeführten Qubits zu berücksichtigen. Das qiskit-addon-cutting-Paket bietet die Hilfsfunktion expand_observables(), die PauliList-Objekte sowie den ursprünglichen und den erweiterten Circuit als Argumente entgegennimmt und eine neue PauliList zurückgibt.
Diese zurückgegebene PauliList enthält keine Informationen über die Koeffizienten der ursprünglichen Observable, diese können jedoch bis zur Rekonstruktion des endgültigen Erwartungswerts ignoriert werden.
# Transform CutWire instructions to Move instructions
qc_2 = cut_wires(qc_1)
# Expand the observable to match the new circuit size
expanded_observable = expand_observables(observable.paulis, qc_0, qc_2)
print(f"Expanded Observable: {expanded_observable}")
qc_2.draw("mpl")
Expanded Observable: ['ZIIIIIIII', 'IIIZIIIII', 'IIIIIIIIZ']
Circuit und Observable partitionieren
Nun kann das Problem in Partitionen aufgeteilt werden. Dies geschieht mit der Funktion partition_problem() und einem optionalen Satz von Partitions-Labels, um festzulegen, wie der Circuit aufgeteilt werden soll. Qubits mit demselben Partitions-Label werden zusammengefasst, und alle nicht-lokalen Gates, die mehr als eine Partition überspannen, werden geschnitten.
Werden keine Partitions-Labels angegeben, wird die Partitionierung automatisch anhand der Konnektivität des Circuits bestimmt. Lies den nächsten Abschnitt über manuelles Schneiden von Wires für weitere Informationen zur Angabe von Partitions-Labels.
partitioned_problem = partition_problem(
circuit=qc_2,
observables=expanded_observable,
)
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables
bases = partitioned_problem.bases
print(f"Subobservables to measure: \n{subobservables}\n")
print(f"Sampling overhead: {np.prod([basis.overhead for basis in bases])}")
subcircuits[0].draw("mpl")
Subobservables to measure:
{0: PauliList(['IIIII', 'ZIIII', 'IIIIZ']), 1: PauliList(['ZIII', 'IIII', 'IIII'])}
Sampling overhead: 256.0
subcircuits[1].draw("mpl")
Bei diesem Partitionierungsschema hast du zwei Wires geschnitten, was zu einem Sampling-Overhead von führt.
Subexperimente generieren, ausführen und Ergebnisse nachverarbeiten
Um den Erwartungswert des vollständigen Circuits zu schätzen, werden aus der gemeinsamen Quasi-Wahrscheinlichkeitsverteilung der zerlegten Gates mehrere Subexperimente erzeugt und anschließend auf einem (oder mehreren) QPUs ausgeführt. Die Methode generate_cutting_experiments erledigt dies, indem sie die oben erstellten Dictionaries subcircuits und subobservables sowie die Anzahl der aus der Verteilung zu ziehenden Samples als Argumente entgegennimmt.
Das Argument num_samples gibt an, wie viele Samples aus der Quasi-Wahrscheinlichkeitsverteilung gezogen werden, und bestimmt die Genauigkeit der für die Rekonstruktion verwendeten Koeffizienten. Die Übergabe von Unendlich (np.inf) stellt sicher, dass alle Koeffizienten exakt berechnet werden. Weitere Informationen findest du in der API-Dokumentation zu Gewichten generieren und Cutting-Experimente generieren.
# Generate subexperiments
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=2**12)
for label, subsystem_subexpts in isa_subexperiments.items()
}
# Retrieve results
results = {label: job.result() for label, job in jobs.items()}
Abschließend kann der Erwartungswert des vollständigen Circuits mit der Methode reconstruct_expectation_values() rekonstruiert werden.
Der folgende Codeblock rekonstruiert die Ergebnisse und vergleicht sie mit dem exakten Erwartungswert.
reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
# Apply the coefficients of the original observable
reconstructed_expval = np.dot(reconstructed_expval_terms, observable.coeffs)
# Compute the exact expectation value using the `qiskit_aer` package.
estimator = EstimatorV2()
exact_expval = estimator.run([(qc_0, 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: 1.45965266
Exact expectation value: 1.59099026
Error in estimation: -0.1313376
Relative error in estimation: -0.08255085
Um den Erwartungswert genau zu rekonstruieren, müssen die Koeffizienten der ursprünglichen Observable (die sich von der Ausgabe von generate_cutting_experiments() unterscheiden) auf die Ausgabe der Rekonstruktion angewendet werden, da diese Information beim Erzeugen der Cutting-Experimente oder beim Erweitern der Observable verloren gegangen ist.
In der Regel können diese Koeffizienten wie oben gezeigt über numpy.dot() angewendet werden.
Wires mit der Low-Level-Move-Instruction manuell schneiden
Eine Einschränkung der höherstufigen CutWire-Instruction ist, dass sie keine Qubit-Wiederverwendung erlaubt. Wenn dies für ein Cutting-Experiment gewünscht ist, kannst du stattdessen Move-Instructions manuell platzieren. Da die Move-Instruction jedoch den Zustand des Ziel-Qubits verwirft, ist es wichtig, dass dieses Qubit keine Verschränkung mit dem Rest des Systems hat. Andernfalls führt die Reset-Operation dazu, dass der Zustand des Circuits nach dem Wire Cut teilweise kollabiert.
Der folgende Codeblock führt einen Wire Cut auf Qubit für denselben Beispiel-Circuit wie zuvor durch. Der Unterschied besteht darin, dass du ein Qubit durch Umkehrung der Move-Operation an der Stelle des zweiten Wire Cuts wiederverwenden kannst (dies ist jedoch nicht immer möglich und hängt vom geschnittenen Circuit ab).
qc_1 = QuantumCircuit(8)
for i in [*range(4), *range(5, 8)]:
qc_1.rx(np.pi / 4, i)
qc_1.cx(0, 3)
qc_1.cx(1, 3)
qc_1.cx(2, 3)
qc_1.append(Move(), [3, 4])
qc_1.cx(4, 5)
qc_1.cx(4, 6)
qc_1.cx(4, 7)
qc_1.append(Move(), [4, 3])
qc_1.cx(0, 3)
qc_1.cx(1, 3)
qc_1.cx(2, 3)
# Expand observable
observable_expanded = SparsePauliOp(["ZIIIIIII", "IIIIZIII", "IIIIIIIZ"])
qc_1.draw("mpl")
Der obige Circuit kann nun partitioniert und Cutting-Experimente generiert werden. Um explizit festzulegen, wie der Circuit partitioniert werden soll, kannst du der Funktion partition_problem() Partitions-Labels hinzufügen. Qubits mit demselben Partitions-Label werden zusammengefasst, und alle nicht-lokalen Gates, die mehr als eine Partition überspannen, werden geschnitten. Die Schlüssel des von partition_problem() ausgegebenen Dictionaries stimmen mit den im Label-String angegebenen überein.
partitioned_problem = partition_problem(
circuit=qc_1,
partition_labels="AAAABBBB",
observables=observable_expanded.paulis,
)
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables
bases = partitioned_problem.bases
print(f"Subobservables to measure: \n{subobservables}\n")
print(f"Sampling overhead: {np.prod([basis.overhead for basis in bases])}")
subcircuits["A"].draw("mpl")
Subobservables to measure:
{'A': PauliList(['IIII', 'ZIII', 'IIIZ']), 'B': PauliList(['ZIII', 'IIII', 'IIII'])}
Sampling overhead: 256.0
subcircuits["B"].draw("mpl")
Nun können die Cutting-Experimente generiert und der Erwartungswert auf dieselbe Weise wie im vorherigen Abschnitt rekonstruiert werden.
Nächste Schritte
- Lies die Anleitung Einstieg in Circuit Cutting mit Gate Cuts.
- Lies das arXiv-Paper über optimales Wire Cutting, um die Äquivalenz zwischen Wire Cutting und Gate Cutting besser zu verstehen.