Zum Hauptinhalt springen

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-cutting verwendest, 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 MjM_j eine Menge von Basen (üblicherweise Pauli X, Y und Z) und PiP_i eine Menge von Eigenzuständen (üblicherweise 0|0\rangle, 1|1\rangle, +|+\rangle und +i|+i\rangle).

wc-1.png wc-2.png

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 θ\theta und ϕ\vec{\phi} parametrisiert. Wenn θ\theta auf 00 gesetzt wird und der Anfangszustand für alle Qubits als 0|0\rangle präpariert wird, beträgt der ideale Erwartungswert von Zi\langle Z_i \rangle für jede Qubit-Position ii unabhängig von den Werten von ϕ\vec{\phi} genau +1+1. 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)

Output of the previous code cell

Wir berechnen den durchschnittlichen Erwartungswert O=1niZiO = \frac{1}{n} \sum_i Z_i über alle Qubits für θ=0\theta = 0. Da der ideale Erwartungswert von Zi=1\langle Z_i \rangle = 1 \forall ii ist, beträgt auch der ideale Erwartungswert von OO genau 11. Die Parameter ϕ\phi 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 n2\frac{n}{2} Qubits automatisch eingefügt wird, wobei nn 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)

Output of the previous code cell

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)

Output of the previous code cell

Observablen erstellen und erweitern

Die Observable, wie zuvor definiert, ist der Durchschnitt von ZZ ü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 II).

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)

Output of the previous code cell

subcircuits[1].draw("mpl", fold=-1)

Output of the previous code cell

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$')

Output of the previous code cell

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()

Output of the previous code cell

uncut_expval
0.9202473958333336

Nächste Schritte

Empfehlungen

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