Wire Cutting zur Schätzung von Erwartungswerten
Geschätzter Aufwand: 22 Sekunden auf einem Heron-Prozessor (HINWEIS: Dies ist nur eine Schätzung. Deine Laufzeit kann variieren.)
Lernziele
Nach diesem Tutorial solltest du Folgendes verstehen:
- Wie du
qiskit-addon-cuttingverwendest, um einen großen Circuit in kleinere Subcircuits aufzuteilen und dadurch den Einfluss von Rauschen zu reduzieren
Voraussetzungen
Wir empfehlen, dass du mit folgendem Thema vertraut bist, bevor du dieses Tutorial durcharbeitest:
- Verwendung des Sampler-Primitives, das in diesem Workflow eingesetzt wird
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 Tutorial 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: [7]) 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 Gates hat, ist er voraussichtlich weniger anfällig für Rauschen. Dieses Tutorial zeigt ein Beispiel, bei dem diese Methode effektiv zur Rauschunterdrückung eingesetzt werden kann.
Anforderungen
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.10.0 oder höher (
pip install qiskit-addon-cutting) - Qiskit addon utils 0.3 oder höher (
pip install qiskit-addon-utils) - Qiskit Aer (
pip install qiskit-aer)
Setup
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-addon-cutting qiskit-aer 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_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, Batch
Kleines Simulator-Beispiel
Dieses Tutorial implementiert ein Qiskit-Muster zur Simulation eines eindimensionalen (1D) Many-Body-Localization-Circuits (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 diesem Circuit findest du in diesem Artikel.
Beachte, dass in einem rauschfreien Simulator der mit und ohne Circuit Cutting erhaltene Erwartungswert gleich ist.
Schritt 1: Klassische Eingaben auf ein Quantenproblem abbilden
Den 1D-MBL-Circuit konstruieren
Zunächst stellen wir eine Funktion zur Konstruktion des 1D-MBL-Circuits 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)
Wir berechnen den durchschnittlichen Erwartungswert über alle Qubits für . Da der ideale Erwartungswert von ist, beträgt auch der ideale Erwartungswert von genau . Die Parameter werden zufällig gewählt.
np.random.seed(42)
phis = list(np.random.rand(mbl.num_parameters - 1))
theta = [0]
params = theta + phis
Der Circuit muss annotiert werden, indem CutWire an den gewünschten Stellen eingefügt wird, um ihn zu partitionieren. In diesem Tutorial entscheiden wir uns für eine gleichmäßige Partition. Der MBL-Circuit ist so gestaltet, dass durch Setzen von use_cut=True in der Funktion die Annotation nach Qubits automatisch eingefügt wird, wobei die Anzahl der Qubits im ursprünglichen Circuit ist. Außerdem haben wir dem Circuit die zufällig generierten Parameter zugewiesen.
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
Den Circuit in kleinere Subcircuits aufteilen
Nun partitionieren wir den Circuit mit qiskit-addon-cutting in zwei kleinere Subcircuits. qiskit-addon-cutting fügt ein virtuelles Move-Gate ein, um die Wire-Cut-Position aufzuteilen, indem die Qubit-Anzahl entsprechend angepasst wird. Wir erstellen nun den Circuit mit diesem virtuellen Gate. Da es einen Wire Cut gibt, erhöht sich die Anzahl der zugehörigen Qubits um 1.
mbl_move = cut_wires(mbl_cut)
mbl_move.draw("mpl", fold=-1)
Observablen erstellen und erweitern
Die Observable, wie zuvor definiert, ist der Durchschnitt von über alle Qubits. Nach dem Einfügen des virtuellen Move-Gates erhöht sich jedoch die effektive Qubit-Anzahl im Circuit. Die Observable muss ebenfalls entsprechend erweitert werden, um diese Änderung der Qubit-Anzahl zu berücksichtigen. Beachte, dass die Observable auf dem zusätzlichen Qubit, das für das virtuelle Move-Gate hinzugefügt wurde, immer trivial wirkt (also als ).
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'])
new_obs = expand_observables(observable, mbl, mbl_move)
new_obs
PauliList(['ZIIIIIIIIII', 'IZIIIIIIIII', 'IIZIIIIIIII', 'IIIZIIIIIII',
'IIIIZIIIIII', 'IIIIIIZIIII', 'IIIIIIIZIII', 'IIIIIIIIZII',
'IIIIIIIIIZI', 'IIIIIIIIIIZ'])
Nun kann der Circuit entlang des Move-Gates partitioniert werden, und wir erhalten die Subcircuits sowie die Subobservablen, also den Anteil der ursprünglichen Observablen, der jedem Subcircuit zugeordnet ist.
partitioned_problem = partition_problem(circuit=mbl_move, observables=new_obs)
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables
Hier visualisieren wir die beiden Subcircuits:
subcircuits[0].draw("mpl", fold=-1)
subcircuits[1].draw("mpl", fold=-1)
Die Erweiterung der Observablen mit der Move-Operation erfordert eine PauliList-Datenstruktur. Zur Rekonstruktion des Erwartungswerts des ursprünglichen Circuits 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 zuvor beschrieben, muss der vorgelagerte Circuit für jeden Schnitt in einer Pauli-Basis gemessen und der nachgelagerte Circuit im Eigenzustand der Basis präpariert werden. Die Funktion generate_cutting_experiments erstellt alle dafür notwendigen Circuits sowie die Koeffizienten, die für die Rekonstruktion jedem Circuit zugeordnet sind. Weitere Details findest du in diesem Paper.
subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits,
observables=subobservables,
num_samples=np.inf,
)
Die Circuits auf das Backend transpilieren
Für das erste Beispiel, das nur die Simulation betrifft, transpilieren wir den Circuit in den Basis-Gate-Satz des Backends:
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=133
)
print(backend)
<IBMBackend('ibm_fez')>
Schritt 3: Ausführung mit Qiskit-Primitiven
Nun führen wir jedes Subexperiment aus:
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: Nachverarbeitung und Rückgabe des Ergebnisses im gewünschten klassischen Format
Nun rufen wir das Ergebnis jedes Subexperiments ab und rekonstruieren den Erwartungswert des ungeschnittenen Circuits:
# 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
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)
Text(0, 0.5, '$M_Z$')
Großes Hardware-Beispiel
Nun demonstrieren wir Wire Cutting für einen 60-Qubit-MBL-Circuit. Der ungeschnittene sowie der geschnittene Circuit 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()
uncut_expval
0.9202473958333336
Nächste Schritte
Wenn du diese Arbeit interessant fandest, könnten dich folgende Materialien interessieren:
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.
[7] Majumdar, R. (2024). Efficient Reduction of Resources and Noise in Discrete Quantum Computing Circuits (Doctoral dissertation, Indian Statistical Institute - Kolkata). https://www.proquest.com/openview/b481def90b1cc80e6b58a77c99e8385c/1?pq-origsite=gscholar&cbl=2026366&diss=y