Drahtschneiden zur Schätzung von Erwartungswerten
Geschätzte Ausführungszeit: 22 Sekunden auf einem Heron-Prozessor (HINWEIS: Dies ist lediglich eine Schätzung. (Die Laufzeit kann variieren.)
Lernziele
- Wie man nutzt
qiskit-addon-cutting, um eine große Schaltung in kleinere Teilschaltungen zu unterteilen und so die Auswirkungen von Rauschen zu verringern
Voraussetzungen
- Verwendung der Sampler-Primitive, die in diesem Arbeitsablauf zum Einsatz kommt
Hintergrund
„Circuit-Knitting“ ist ein Oberbegriff, der verschiedene Methoden umfasst, bei denen eine Schaltung in mehrere kleinere Teilschaltungen mit weniger Logikgattern oder Qubits unterteilt wird. Jede der Teilschaltungen kann unabhängig ausgeführt werden, und das Endergebnis ergibt sich aus einer klassischen Nachbearbeitung der Ergebnisse der einzelnen Teilschaltungen. Diese Technik ist im Qiskit-Add-on „Circuit cutting“ verfügbar; eine ausführliche Erläuterung der Technik finden Sie in der Dokumentation sowie in weiteren Einführungsmaterialien.
Dieses Tutorial befasst sich mit einer Methode namens „Wire Cutting“, bei der die Schaltung entlang der Leiterbahn unterteilt wird [1], [2]. Beachten Sie, dass die Teilung in klassischen Schaltungen einfach ist, da das Ergebnis am Teilungspunkt deterministisch bestimmt werden kann und entweder 0 oder 1 ist. Der Zustand des Qubits zum Zeitpunkt des Schnitts ist jedoch im Allgemeinen ein gemischter Zustand. Daher muss jeder Teilkreis mehrfach in verschiedenen Basiszuständen (in der Regel in einer tomographisch vollständigen Basis, wie beispielsweise der Pauli-Basis [3], [4] ) gemessen und entsprechend in seinem Eigenzustand vorbereitet werden. Die folgende Abbildung (Quelle: [7] ) zeigt ein Beispiel für die Aufteilung eines GHZ-Zustands mit vier Qubits in drei Teilschaltungen. Hier bezeichnet eine Menge von Basisvektoren (in der Regel Pauli X, Y und Z), und bezeichnet eine Menge von Eigenzuständen (in der Regel , , und ).

Da jede Teilschaltung über weniger Qubits und Gatter verfügt, ist davon auszugehen, dass sie weniger anfällig für Rauschen ist. Dieses Tutorial zeigt ein Beispiel, wie diese Methode eingesetzt werden kann, um das Rauschen im System wirksam zu unterdrücken.
Anforderungen
Bevor Sie mit diesem Tutorial beginnen, sollten Sie sicherstellen, dass Sie Folgendes installiert haben:
- Qiskit SDK v2.0 oder später, mit Visual isierungsunterstützung
- Qiskit Runtime v0.22 oder später (
pip install qiskit-ibm-runtime) - Schaltkreis-Erweiterung für Qiskit v0.10.0 oder höher (
pip install qiskit-addon-cutting) - Qiskit-Add-on „utils“ 0.3 oder höher (
pip install qiskit-addon-utils) - Qiskit Aer (
pip install qiskit-aer)
Einrichtung
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_aer import AerSimulator
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, BatchBeispiel für einen Simulator im kleinen Maßstab
In diesem Tutorial wird ein Qiskit-Muster zur Simulation einer eindimensionalen ( 1D en) Many-Body-Localization-Schaltung (MBL) implementiert. Der MBL-Schaltkreis ist hardwareeffizient und wird durch die beiden Parameter und parametrisiert. Wenn auf gesetzt und der Anfangszustand für alle Qubits auf vorbereitet wird, beträgt der ideale Erwartungswert von für jedes Qubit } unabhängig von den Werten von . Weitere Details zu diesem Schaltkreis finden Sie in diesem Artikel.
Beachten Sie, dass in einem rauschfreien Simulator der Erwartungswert mit und ohne Schaltungsunterbrechung identisch ist.
Schritt 1: Klassische Eingaben einem Quantenproblem zuordnen
Bauen Sie die MBL-Schaltung „ 1D “ auf
Zunächst stellen wir eine Funktion zur Erstellung der MBL-Schaltung nach „ 1D “ vor.
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)num_qubits = 10
depth = 2
mbl = MBLChainCircuit(num_qubits, depth)
mbl.draw("mpl", fold=-1)Output:
Wir berechnen den durchschnittlichen Erwartungswert über alle Qubits für . Da der ideale Erwartungswert von ist, beträgt der ideale Erwartungswert von ebenfalls . Die Parameter werden zufällig ausgewählt.
np.random.seed(42)
phis = list(np.random.rand(mbl.num_parameters - 1))
theta = [0]
params = theta + phisUm die Schaltung zu unterteilen, muss sie durch Einfügen von „ CutWire “ an den gewünschten Stellen kommentiert werden. Für dieses Tutorial entscheiden wir uns für eine gleichmäßige Aufteilung. Der MBL-Schaltkreis ist so ausgelegt, dass die Einstellung use_cut=True in der Funktion die Anmerkung korrekt nach Qubits einfügt, wobei die Anzahl der Qubits im ursprünglichen Schaltkreis ist. Außerdem haben wir die zufällig generierten Parameter der Schaltung zugewiesen.
mbl_cut = MBLChainCircuit(num_qubits, depth, use_cut=True)
mbl_cut.assign_parameters(params, inplace=True)
mbl_cut.draw("mpl", fold=-1)Output:
Schritt 2: Optimierung des Problems für die Ausführung auf Quantenhardware
Schneiden Sie den Schaltkreis in kleinere Teilkreise
qiskit-addon-cuttingNun unterteilen wir den Schaltkreis mithilfe von in zwei kleinere Teilschaltkreise. qiskit-addon-cutting fügt ein virtuelles Move Gatter hinzu, um die Stelle des Drahtschnitts durch entsprechende Anpassung der Anzahl der Qubits zu teilen. Nun erstellen wir die Schaltung mit diesem virtuellen Gatter. Da ein Draht durchtrennt ist, erhöht sich die Anzahl der zugehörigen Qubits um 1.
mbl_move = cut_wires(mbl_cut)
mbl_move.draw("mpl", fold=-1)Output:
Den Observablen konstruieren und erweitern
Die zuvor definierte Observable ist der Mittelwert von „ “ für jedes Qubit. Durch das Einfügen des virtuellen Move Gatters erhöht sich jedoch die effektive Anzahl der Qubits in der Schaltung. Der Observabel muss ebenfalls entsprechend erweitert werden, um dieser Änderung der Anzahl der Qubits Rechnung zu tragen. Beachte, dass die Beobachtungsgröße auf das für das virtuelle Move Gatter hinzugefügte zusätzliche Qubit stets trivial wirkt (wie in ).
observable = PauliList(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)]
)
observableOutput:
PauliList(['ZIIIIIIIII', 'IZIIIIIIII', 'IIZIIIIIII', 'IIIZIIIIII',
'IIIIZIIIII', 'IIIIIZIIII', 'IIIIIIZIII', 'IIIIIIIZII',
'IIIIIIIIZI', 'IIIIIIIIIZ'])
new_obs = expand_observables(observable, mbl, mbl_move)
new_obsOutput:
PauliList(['ZIIIIIIIIII', 'IZIIIIIIIII', 'IIZIIIIIIII', 'IIIZIIIIIII',
'IIIIZIIIIII', 'IIIIIIZIIII', 'IIIIIIIZIII', 'IIIIIIIIZII',
'IIIIIIIIIZI', 'IIIIIIIIIIZ'])
Nun kann die Schaltung entlang des Move Gates unterteilt werden, und wir erhalten die Teilschaltungen sowie die Teilobservable, also den Teil der ursprünglichen Observable, der jeder Teilschaltung zugeordnet ist.
partitioned_problem = partition_problem(circuit=mbl_move, observables=new_obs)
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservablesHier stellen wir die beiden Teilschaltungen dar:
subcircuits[0].draw("mpl", fold=-1)Output:
subcircuits[1].draw("mpl", fold=-1)Output:
Um das Beobachtbare mithilfe der Move Operation zu erweitern, ist eine PauliList Datenstruktur erforderlich. Um den Erwartungswert der ursprünglichen Schaltung zu rekonstruieren, benötigen wir die Observable im SparsePauliOp Format.
M_z = SparsePauliOp(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)],
coeffs=[1 / num_qubits] * num_qubits,
)Wie bereits erwähnt, muss für jeden Schnitt der vorgelagerte Kreis in einer Pauli-Basis gemessen werden, und der nachgelagerte Kreis muss im Eigenzustand dieser Basis vorbereitet werden. Die Funktion generate_cutting_experiments erstellt alle erforderlichen Schaltungen sowie die zu jeder Schaltung gehörenden Koeffizienten, die für die Rekonstruktion benötigt werden. Weitere Einzelheiten finden Sie in diesem Artikel.
subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits,
observables=subobservables,
num_samples=np.inf,
)Die Schaltkreise auf das Backend transpilieren
Für das erste Beispiel, bei dem es ausschließlich um die Simulation geht, transpilieren wir die Schaltung in den Basis-Gatesatz des Backends:
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=133
)
print(backend)Output:
<IBMBackend('ibm_fez')>
Schritt 3: Mit „ Qiskit primitives “ ausführen
Führen Sie nun jedes Teilversuch durch:
pm_basis = generate_preset_pass_manager(
optimization_level=2, basis_gates=backend.configuration().basis_gates
)
basis_subexperiments = {
label: pm_basis.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}sampler = SamplerV2(mode=AerSimulator())
jobs = {
label: sampler.run(subsystem_subexpts, shots=2**12)
for label, subsystem_subexpts in basis_subexperiments.items()
}Schritt 4: Nachbearbeitung und Rückgabe des Ergebnisses im gewünschten klassischen Format
Nun holen wir das Ergebnis jedes einzelnen Durchlaufs der Teilversuche ab und berechnen den Erwartungswert der ungeschnittenen Schaltung:
# 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_expvalOutput:
np.float64(0.9953821063041687)
methods = [
"Uncut",
"Wire cut",
]
values = [
1,
reconstructed_expval,
] # since the ideal expectation value in noiseless simulation is +1
ax = plt.gca()
plt.bar(methods, values, color="#a56eff", width=0.4, edgecolor="#8a3ffc")
ax.set_ylabel(r"$M_Z$", fontsize=12)Output:
Text(0, 0.5, '$M_Z$')
Beispiel für groß angelegte Hardware
Nun demonstrieren wir das Drahtschneiden für eine MBL-Schaltung mit 60 Qubits. Sowohl die ungeschnittenen als auch die geschnittenen Schaltkreise werden auf „ IBM Quantum® “-Hardware ausgeführt:
num_qubits = 60
depth = 2
# construct the circuit
mbl = MBLChainCircuit(num_qubits, depth)
# create parameters
phis = list(np.random.rand(mbl.num_parameters - 1))
theta = [0]
params = theta + phis
# construct the cut circuit
mbl_cut = MBLChainCircuit(num_qubits, depth, use_cut=True)
mbl_cut.assign_parameters(params, inplace=True)
mbl_move = cut_wires(mbl_cut)
# Define observable and expand to account for the wire cut
observable = PauliList(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)]
)
new_obs = expand_observables(observable, mbl, mbl_move)
# Construct a SparsePauliOp version of the observable for later use in reconstruction
M_z = SparsePauliOp(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)],
coeffs=[1 / num_qubits] * num_qubits,
)
# Partition the circuit and get subcircuits and subobservables
partitioned_problem = partition_problem(circuit=mbl_move, observables=new_obs)
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables
# Obtain subexperiments and coefficients
subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits,
observables=subobservables,
num_samples=np.inf,
)
# Transpile the subexperiments to the backend
pm = generate_preset_pass_manager(optimization_level=2, backend=backend)
isa_subexperiments = {
label: pm.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}
# Execute the subexperiments and retrieve results
with Batch(backend=backend) as batch:
sampler = SamplerV2(mode=batch)
sampler.options.environment.job_tags = ["TUT_WC"]
jobs = {
label: sampler.run(subsystem_subexpts, shots=2**12)
for label, subsystem_subexpts in isa_subexperiments.items()
}
results = {label: job.result() for label, job in jobs.items()}
# Reconstruct the expectation value of the original observable
reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
reconstructed_expval = np.dot(reconstructed_expval_terms, M_z.coeffs).real
# Compute the uncut circuit to obtain the noisy expectation value for comparison
sampler = SamplerV2(mode=backend)
sampler.options.environment.job_tags = ["TUT_WC"]
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)
# visualize the results
ax = plt.gca()
methods = ["uncut", "cut"]
values = [uncut_expval, reconstructed_expval]
plt.bar(methods, values, color="#a56eff", width=0.4, edgecolor="#8a3ffc")
plt.axhline(y=1, color="k", linestyle="--")
plt.text(0.3, 0.95, "Exact result")
plt.show()Output:
uncut_expvalOutput:
0.9202473958333336
Nächste Schritte
Wenn Sie diese Arbeit interessant fanden, könnten Sie auch an folgenden Materialien interessiert sein:
Referenzen
[1] Peng, T., Harrow, A. W., Ozols, M., & Wu, X. (2020). Simulation großer Quantenschaltungen auf einem kleinen Quantencomputer. Physical Review Letters, 125(15), 150504.
[2] Tang, W., Tomesh, T., Suchara, M., Larson, J., & Martonosi, M. (2021, April). Cutqc: Verwendung kleiner Quantencomputer für die Bewertung großer Quantenschaltungen. In Proceedings of the 26th ACM International conference on architectural support for programming languages and operating systems (S. 473-486).
[3] Perlin, M. A., Saleem, Z. H., Suchara, M., & Osborn, J. C. (2021). Quantum Circuit Cutting mit Maximum-Likelihood-Tomographie. npj Quantum Information, 7(1), 64.
[4] Majumdar, R., & Wood, C. J. (2022). Fehlervermindertes Schneiden von Quantenschaltungen. arXiv vorabdruck arXiv:2211.13431.
[5] Khare, T., Majumdar, R., Sangle, R., Ray, A., Seshadri, P. V., & Simmhan, Y. (2023). Parallelisierung quantenklassischer Workloads: Profiling der Auswirkungen von Splitting-Techniken. 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). Verteiltes Scheduling von Quantenschaltungen mit Rauschen und Zeitoptimierung. arXiv vorabdruck arXiv:2309.06005.
[7] Majumdar, R. (2024). Effiziente Reduzierung von Ressourcen und Rauschen in Schaltkreisen für diskrete Quantencomputer (Doktorarbeit, Indian Statistical Institute – Kalkutta). https://www.proquest.com/openview/b481def90b1cc80e6b58a77c99e8385c/1?pq-origsite=gscholar&cbl=2026366&diss=y