Wire Cutting zur Schätzung von Erwartungswerten
Geschätzter Aufwand: eine Minute auf einem Eagle-Prozessor (HINWEIS: Dies ist nur eine Schätzung. Deine Laufzeit kann variieren.)
Hintergrund
Circuit Knitting ist ein Oberbegriff, der verschiedene Methoden zur Partitionierung eines Circuits in mehrere kleinere Subcircuits mit weniger Gates und/oder Qubits umfasst. Jeder der Subcircuits kann unabhängig ausgeführt werden, und das Endergebnis wird durch klassische Nachverarbeitung der Ergebnisse der einzelnen Subcircuits gewonnen. Diese Technik ist über das Circuit Cutting Qiskit Addon zugänglich; eine detaillierte Erklärung der Technik findest du in der Dokumentation sowie in weiterem Einführungsmaterial.
Dieses Notebook behandelt eine Methode namens Wire Cutting, bei der der Circuit entlang der Leitung partitioniert wird [1], [2]. Beachte, dass die Partitionierung bei klassischen Circuits einfach ist, da das Ergebnis an der Trennstelle deterministisch bestimmt werden kann und entweder 0 oder 1 ist. Der Zustand des Qubits an der Schnittstelle ist jedoch im Allgemeinen ein gemischter Zustand. Daher muss jeder Subcircuit mehrfach in verschiedenen Basen gemessen werden (üblicherweise ein tomographisch vollständiger Basissatz wie die Pauli-Basis [3], [4]) und entsprechend in seinem Eigenzustand präpariert werden. Die folgende Abbildung (Quelle: PhD-Thesis, Ritajit Majumdar) zeigt ein Beispiel für Wire Cutting eines 4-Qubit-GHZ-Zustands in drei Subcircuits. Hierbei bezeichnen eine Menge von Basen (üblicherweise Pauli X, Y und Z) und eine Menge von Eigenzuständen (üblicherweise , , und ).
Da jeder Subcircuit weniger Qubits und/oder Gates hat, ist er voraussichtlich weniger anfällig für Rauschen. Dieses Notebook zeigt ein Beispiel, bei dem diese Methode effektiv zur Rauschunterdrückung eingesetzt werden kann.
Voraussetzungen
Stelle vor Beginn dieses Tutorials sicher, dass Folgendes installiert ist:
- Qiskit SDK v2.0 oder höher, mit Visualisierungs-Unterstützung
- 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)
Wir betrachten für dieses Notebook einen Many-Body-Localization-Circuit (MBL). Der MBL-Circuit ist ein hardwareeffizienter Circuit und durch zwei Parameter und parametrisiert. Wenn auf gesetzt wird und der Anfangszustand für alle Qubits als präpariert wird, beträgt der ideale Erwartungswert von für jede Qubit-Position unabhängig von den Werten von genau . Weitere Details zu MBL-Circuits findest du in diesem Paper.
Setup
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-addon-cutting qiskit-ibm-runtime
import numpy as np
import matplotlib.pyplot as plt
from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit
from qiskit.quantum_info import PauliList, SparsePauliOp
from qiskit.transpiler import generate_preset_pass_manager
from qiskit.result import sampled_expectation_value
from qiskit_addon_cutting.instructions import CutWire
from qiskit_addon_cutting import (
cut_wires,
expand_observables,
partition_problem,
generate_cutting_experiments,
reconstruct_expectation_values,
)
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import SamplerV2, Batch
class MBLChainCircuit(QuantumCircuit):
def __init__(
self, num_qubits: int, depth: int, use_cut: bool = False
) -> None:
super().__init__(
num_qubits, name=f"MBLChainCircuit<{num_qubits}, {depth}>"
)
evolution = MBLChainEvolution(num_qubits, depth, use_cut)
self.compose(evolution, inplace=True)
class MBLChainEvolution(QuantumCircuit):
def __init__(self, num_qubits: int, depth: int, use_cut) -> None:
super().__init__(
num_qubits, name=f"MBLChainEvolution<{num_qubits}, {depth}>"
)
theta = Parameter("θ")
phis = ParameterVector("φ", num_qubits)
for layer in range(depth):
layer_parity = layer % 2
# print("layer parity", layer_parity)
for qubit in range(layer_parity, num_qubits - 1, 2):
# print(qubit)
self.cz(qubit, qubit + 1)
self.u(theta, 0, np.pi, qubit)
self.u(theta, 0, np.pi, qubit + 1)
if (
use_cut
and layer_parity == 0
and (
qubit == num_qubits // 2 - 1
or qubit == num_qubits // 2
)
):
self.append(CutWire(), [num_qubits // 2])
if use_cut and layer < depth - 1 and layer_parity == 1:
if qubit == num_qubits // 2:
self.append(CutWire(), [qubit])
for qubit in range(num_qubits):
self.p(phis[qubit], qubit)
Teil I. Kleines Beispiel
Schritt 1: Klassische Eingaben auf ein Quantenproblem abbilden
Zunächst erstellen wir einen Template-Circuit ohne konkrete Parameterwerte. Wir fügen auch Platzhalter namens CutWire ein, um die Position der Schnitte zu markieren. Für das kleine Beispiel verwenden wir einen 10-Qubit-MBL-Circuit.
num_qubits = 10
depth = 2
mbl = MBLChainCircuit(num_qubits, depth)
mbl.draw("mpl", fold=-1)
Zur Erinnerung: Wir möchten den Erwartungswert der Observablen für bestimmen. Für den Parameter verwenden wir zufällige Werte.
phis = list(np.random.rand(mbl.num_parameters - 1))
theta = [0]
params = theta + phis
params
[0,
0.2376615174332788,
0.28244289857682414,
0.019248960591717768,
0.46140600996102477,
0.31408025180068433,
0.718184005135733,
0.991153920182475,
0.09289485768301442,
0.8857848280067783,
0.6177529765767047]
Nun annotieren wir den Circuit für das Cutting, indem wir geeignete CutWire-Elemente einfügen, um zwei ungefähr gleich große Teile zu erzeugen. Wir setzen use_cut=True in der Funktion und lassen die Annotation nach Qubits einfügen, wobei die Anzahl der Qubits im ursprünglichen Circuit ist.
mbl_cut = MBLChainCircuit(num_qubits, depth, use_cut=True)
mbl_cut.assign_parameters(params, inplace=True)
mbl_cut.draw("mpl", fold=-1)
Schritt 2: Problem für die Ausführung auf Quantenhardware optimieren
Als nächstes schneiden wir den Circuit in zwei kleinere Subcircuits auf. Für dieses Beispiel beschränken wir uns auf zwei Subcircuits. Dazu verwenden wir das Qiskit Addon: Circuit Cutting.
Den Circuit in kleinere Subcircuits aufteilen
Das Schneiden einer Leitung an einer Stelle erhöht die Qubit-Anzahl um eins. Neben dem ursprünglichen Qubit gibt es nun ein zusätzliches Qubit als Platzhalter im Circuit nach dem Schnitt. Die folgende Abbildung veranschaulicht dies:
Dieses Addon verwendet die Funktion cut_wires, um die zusätzlichen Qubits zu berücksichtigen, die durch das Cutting entstehen.
mbl_move = cut_wires(mbl_cut)
Observablen erstellen und erweitern
Nun konstruieren wir die Observable . Da das ideale Ergebnis von für jedes gleich ist, beträgt das ideale Ergebnis von ebenfalls .
observable = PauliList(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)]
)
observable
PauliList(['ZIIIIIIIII', 'IZIIIIIIII', 'IIZIIIIIII', 'IIIZIIIIII',
'IIIIZIIIII', 'IIIIIZIIII', 'IIIIIIZIII', 'IIIIIIIZII',
'IIIIIIIIZI', 'IIIIIIIIIZ'])
Beachte jedoch, dass die Anzahl der Qubits im Circuit nach dem Einfügen der virtuellen 2-Qubit-Move-Operationen durch das Cutting gestiegen ist. Daher müssen wir auch die Observablen erweitern, indem wir Identitäten einfügen, um sie an den aktuellen Circuit anzupassen.
new_obs = expand_observables(observable, mbl, mbl_move)
new_obs
PauliList(['ZIIIIIIIIII', 'IZIIIIIIIII', 'IIZIIIIIIII', 'IIIZIIIIIII',
'IIIIZIIIIII', 'IIIIIIZIIII', 'IIIIIIIZIII', 'IIIIIIIIZII',
'IIIIIIIIIZI', 'IIIIIIIIIIZ'])
Beachte, dass jede Observable nun auf sieben Qubits erweitert wurde, entsprechend dem Circuit mit der Move-Operation, anstatt der ursprünglichen 6 Qubits. Als nächstes partitionieren wir den Circuit in zwei Subcircuits.
partitioned_problem = partition_problem(circuit=mbl_move, observables=new_obs)
Wir visualisieren die Subcircuits.
subcircuits = partitioned_problem.subcircuits
subcircuits[0].draw("mpl", fold=-1)
subcircuits[1].draw("mpl", fold=-1)
Die Observablen wurden ebenfalls partitioniert, um zu den Subcircuits zu passen.
subobservables = partitioned_problem.subobservables
subobservables
{0: PauliList(['IIIIII', 'IIIIII', 'IIIIII', 'IIIIII', 'IIIIII', 'IZIIII',
'IIZIII', 'IIIZII', 'IIIIZI', 'IIIIIZ']),
1: PauliList(['ZIIII', 'IZIII', 'IIZII', 'IIIZI', 'IIIIZ', 'IIIII', 'IIIII',
'IIIII', 'IIIII', 'IIIII'])}
Beachte, dass jeder Subcircuit zu einer Anzahl von Stichproben führt. Die Rekonstruktion berücksichtigt das Ergebnis jeder dieser Stichproben. Jede dieser Stichproben wird als subexperiment bezeichnet.
Die Erweiterung der Observablen mit der Move-Operation erfordert eine PauliList-Datenstruktur. Wir können die -Observable auch in der allgemeineren SparsePauliOp-Datenstruktur erstellen, was später bei der Rekonstruktion der Subexperimente nützlich sein wird.
M_z = SparsePauliOp(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)],
coeffs=[1 / num_qubits] * num_qubits,
)
M_z
SparsePauliOp(['ZIIIIIIIII', 'IZIIIIIIII', 'IIZIIIIIII', 'IIIZIIIIII', 'IIIIZIIIII', 'IIIIIZIIII', 'IIIIIIZIII', 'IIIIIIIZII', 'IIIIIIIIZI', 'IIIIIIIIIZ'],
coeffs=[0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j,
0.1+0.j, 0.1+0.j])
subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits,
observables=subobservables,
num_samples=np.inf,
)
Sehen wir uns zwei Beispiele an, bei denen die geschnittenen Qubits in zwei verschiedenen Basen gemessen werden. Zunächst wird in der normalen Z-Basis gemessen, dann in der X-Basis.
subexperiments[0][6].draw("mpl", fold=-1)
subexperiments[0][2].draw("mpl", fold=-1)
Jedes Subexperiment transpilieren
Derzeit müssen wir unsere Circuits transpilieren, bevor wir sie zur Ausführung einreichen. Daher transpilieren wir zunächst jeden Circuit in den Subexperimenten.
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=127
)
Nun müssen wir jeden Circuit in den Subexperimenten transpilieren. Dazu erstellen wir zunächst einen Pass Manager und verwenden ihn dann zum Transpilieren der Circuits.
pm = generate_preset_pass_manager(optimization_level=2, backend=backend)
isa_subexperiments = {
label: pm.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}
isa_subexperiments[0][0].draw("mpl", fold=-1, idle_wires=False)
Schritt 3: Ausführung mit Qiskit-Primitiven
Nun führen wir jeden Circuit im Subexperiment aus. Qiskit-addon-cutting verwendet SamplerV2 zur Ausführung der Subexperimente.
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()
}
Schritt 4: Nachverarbeitung und Rückgabe des Ergebnisses im gewünschten klassischen Format
Nachdem die Circuits ausgeführt wurden, müssen wir die Ergebnisse abrufen und den Erwartungswert für den ungeschnittenen Circuit und die ursprüngliche Observable rekonstruieren.
# Retrieve results
results = {label: job.result() for label, job in jobs.items()}
reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
reconstructed_expval = np.dot(reconstructed_expval_terms, M_z.coeffs).real
reconstructed_expval
0.9674376845359803
Gegenprüfung
Führen wir nun den Circuit ohne Cutting aus und überprüfen das dortige Ergebnis. Beachte, dass wir für die Ausführung des ungeschnittenen Circuits direkt EstimatorV2 zur Berechnung der Erwartungswerte verwenden könnten. Wir verwenden jedoch durchgehend dasselbe Primitive. Daher nutzen wir SamplerV2, um die Wahrscheinlichkeitsverteilung zu erhalten und den Erwartungswert mit der Funktion sampled_expectation_value zu berechnen.
Zunächst müssen wir den ungeschnittenen mbl-Circuit transpilieren.
sampler = SamplerV2(mode=backend)
if mbl.num_clbits == 0:
mbl.measure_all()
isa_mbl = pm.run(mbl)
Als nächstes konstruieren wir den pub und führen den ungeschnittenen Circuit aus.
pub = (isa_mbl, params)
uncut_job = sampler.run([pub])
uncut_counts = uncut_job.result()[0].data.meas.get_counts()
uncut_expval = sampled_expectation_value(uncut_counts, M_z)
uncut_expval
0.9498046875000001
Wir stellen fest, dass der über Wire Cutting ermittelte Erwartungswert näher am idealen Wert von liegt als der des ungeschnittenen Circuits. Skalieren wir nun das Problem nach oben.
Teil II. Skalierung!
Zuvor haben wir die Ergebnisse für einen 10-Qubit-MBL-Circuit gezeigt. Im Folgenden zeigen wir, dass die Verbesserung des Erwartungswerts auch für größere Circuits erzielt wird. Dazu wiederholen wir den Prozess für einen 60-Qubit-MBL-Circuit.
Schritt 1: Klassische Eingaben auf ein Quantenproblem abbilden
num_qubits = 60
depth = 2
mbl = MBLChainCircuit(num_qubits, depth)
Wir erstellen einen zufälligen Satz von Werten für .
phis = list(np.random.rand(mbl.num_parameters - 1))
theta = [0]
params = theta + phis
Als nächstes konstruieren wir den geschnittenen Circuit.
mbl_cut = MBLChainCircuit(num_qubits, depth, use_cut=True)
mbl_cut.assign_parameters(params, inplace=True)
mbl_cut.draw("mpl", fold=-1)
Schritt 2: Problem für die Ausführung auf Quantenhardware optimieren
Wie beim kleinen Beispiel partitionieren wir den Circuit und die Observable für die Cutting-Experimente.
mbl_move = cut_wires(mbl_cut)
# Define observable
observable = PauliList(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)]
)
new_obs = expand_observables(observable, mbl, mbl_move)
# Partition the circuit into subcircuits
partitioned_problem = partition_problem(circuit=mbl_move, observables=new_obs)
# Get subcircuits
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables
Wir erstellen außerdem ein SparsePauliOp-Objekt für die Observable mit den entsprechenden Koeffizienten.
M_z = SparsePauliOp(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)],
coeffs=[1 / num_qubits] * num_qubits,
)
Als nächstes generieren wir die Subexperimente und transpilieren jeden Circuit im Subexperiment.
subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits,
observables=subobservables,
num_samples=np.inf,
)
isa_subexperiments = {
label: pm.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}
Schritt 3: Ausführung mit Qiskit-Primitiven
Wir verwenden den Batch-Modus, um alle Circuits in den Subexperimenten auszuführen.
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()
}
Schritt 4: Nachverarbeitung und Rückgabe des Ergebnisses im gewünschten klassischen Format
Rufen wir nun die Ergebnisse für jeden Circuit im Subexperiment ab und rekonstruieren den Erwartungswert für den ungeschnittenen Circuit und die ursprüngliche Observable.
# Retrieve results
results = {label: job.result() for label, job in jobs.items()}
reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
reconstructed_expval = np.dot(reconstructed_expval_terms, M_z.coeffs).real
reconstructed_expval
0.9631355921427409
Gegenprüfung
Wie beim kleinen Beispiel ermitteln wir nochmals den Erwartungswert durch Ausführung des ungeschnittenen Circuits und vergleichen das Ergebnis mit dem Circuit Cutting. Wir verwenden SamplerV2, um eine einheitliche Nutzung der Primitiven beizubehalten.
sampler = SamplerV2(mode=backend)
if mbl.num_clbits == 0:
mbl.measure_all()
isa_mbl = pm.run(mbl)
pub = (isa_mbl, params)
uncut_job = sampler.run([pub])
uncut_counts = uncut_job.result()[0].data.meas.get_counts()
uncut_expval = sampled_expectation_value(uncut_counts, M_z)
uncut_expval
0.9426757812499998
Visualisierung
Visualisieren wir die Verbesserung des Erwartungswerts durch Wire Cutting.
ax = plt.gca()
methods = ["cut", "uncut"]
values = [reconstructed_expval, uncut_expval]
plt.bar(methods, values, color="#a56eff", width=0.4, edgecolor="#8a3ffc")
plt.axhline(y=1, color="k", linestyle="--")
ax.set_ylim([0.85, 1.02])
plt.text(0.3, 0.99, "Exact result")
plt.show()
Schlussfolgerung
Wir beobachten, dass Wire Cutting sowohl bei kleinen als auch bei großen Problemen zu besseren Ergebnissen führt als die ungeschnittene Variante. Beachte, dass für diese Experimente keine Fehlerminderungstechniken eingesetzt wurden. Die erzielte Verbesserung ist daher ausschließlich auf das Wire Cutting zurückzuführen. Es ist möglich, die Ergebnisse durch den Einsatz verschiedener Minderungsmethoden in Kombination mit Circuit Cutting weiter zu verbessern.
Darüber hinaus haben wir in diesem Notebook beide Subcircuits auf derselben Hardware ausgeführt. In [5], [6] zeigen die Autoren eine Methode zur Verteilung der Subcircuits auf verschiedene Hardware unter Verwendung von Rauschinformationen, um die Rauschunterdrückung zu maximieren und den Prozess zu parallelisieren.
Anhang: Überlegungen zur Ressourcenskalierung
Die Anzahl der auszuführenden Circuits wächst mit der Anzahl der Schnitte. Obwohl viele Schnitte kleine Subcircuits erzeugen und damit die Leistung weiter verbessern können, führen sie auch zu einer erheblich hohen Anzahl von Circuit-Ausführungen, was in den meisten Fällen nicht praktikabel ist. Im Folgenden zeigen wir ein Beispiel für die Anzahl der Subcircuits in Abhängigkeit von der Anzahl der Schnitte für einen 50-Qubit-Circuit.
Beachte, dass selbst bei fünf Schnitten die Anzahl der Subexperimente bei etwa 200.000 liegt. Daher sollte Circuit Cutting nur dann eingesetzt werden, wenn die Anzahl der Schnitte gering ist.
Je ein Beispiel für schnittfreundliche und schnittunfreundliche Circuits
Schnittfreundlicher Circuit
Wie bereits erwähnt, ist ein Circuit schnittfreundlich, wenn er mit einer kleinen Anzahl von Schnitten in kleinere, disjunkte Subcircuits partitioniert werden kann. Jeder hardwareeffiziente Circuit, also ein Circuit, der bei der Abbildung auf die Hardware-Koppelstruktur wenig bis keine SWAP-Gates benötigt, ist im Allgemeinen schnittfreundlich. Im Folgenden zeigen wir ein Beispiel eines exzitationserhaltenden Ansatzes (Excitation Preserving Ansatz), der in der Quantenchemie verwendet wird. Beachte, dass ein solcher Circuit unabhängig von der Anzahl der Qubits mit einem einzigen Schnitt in zwei Subcircuits partitioniert werden kann.

Schnittunfreundlicher Circuit
Ein Circuit ist schnittunfreundlich, wenn die zur Bildung disjunkter Partitionen benötigte Anzahl von Schnitten im Allgemeinen mit der Tiefe oder der Qubit-Anzahl erheblich wächst. Beachte, dass mit jedem Schnitt ein zusätzliches Qubit benötigt wird. Mit steigender Schnittanzahl erhöht sich also auch die effektive Qubit-Anzahl. Im Folgenden zeigen wir ein Beispiel eines 3-Qubit-Grover-Circuits mit einer möglichen Cutting-Instanz.
Wir stellen fest, dass drei Schnitte erforderlich sind und der Schnitt eher vertikal als horizontal verläuft. Dies bedeutet, dass die Anzahl der Schnitte erwartungsgemäß linear mit der Anzahl der Qubits skaliert, was für das Cutting ungeeignet ist.
Referenzen
[1] Peng, T., Harrow, A. W., Ozols, M., & Wu, X. (2020). Simulating large quantum circuits on a small quantum computer. Physical review letters, 125(15), 150504.
[2] Tang, W., Tomesh, T., Suchara, M., Larson, J., & Martonosi, M. (2021, April). Cutqc: using small quantum computers for large quantum circuit evaluations. In Proceedings of the 26th ACM International conference on architectural support for programming languages and operating systems (pp. 473-486).
[3] Perlin, M. A., Saleem, Z. H., Suchara, M., & Osborn, J. C. (2021). Quantum circuit cutting with maximum-likelihood tomography. npj Quantum Information, 7(1), 64.
[4] Majumdar, R., & Wood, C. J. (2022). Error mitigated quantum circuit cutting. arXiv preprint arXiv:2211.13431.
[5] Khare, T., Majumdar, R., Sangle, R., Ray, A., Seshadri, P. V., & Simmhan, Y. (2023). Parallelizing Quantum-Classical Workloads: Profiling the Impact of Splitting Techniques. In 2023 IEEE International Conference on Quantum Computing and Engineering (QCE) (Vol. 1, pp. 990-1000). IEEE.
[6] Bhoumik, D., Majumdar, R., Saha, A., & Sur-Kolay, S. (2023). Distributed Scheduling of Quantum Circuits with Noise and Time Optimization. arXiv preprint arXiv:2309.06005.
Tutorial-Umfrage
Nimm dir bitte einen Moment Zeit für diese kurze Umfrage, um Feedback zu diesem Tutorial zu geben. Deine Rückmeldungen helfen uns, unsere Inhalte und die Nutzererfahrung zu verbessern.