Kombination von Fehlerminderungsoptionen mit dem Estimator-Primitive
Nutzungsschätzung: Sieben Minuten auf einem Heron r2-Prozessor (HINWEIS: Dies ist nur eine Schätzung. Deine Laufzeit kann variieren.)
Hintergrund
Dieser Walkthrough untersucht die Fehlerunterdrückungs- und Fehlerminderungsoptionen, die mit dem Estimator-Primitive von Qiskit Runtime verfügbar sind. Du wirst eine Schaltung und eine Observable konstruieren und Jobs mit dem Estimator-Primitive unter Verwendung verschiedener Kombinationen von Fehlerminderungseinstellungen einreichen. Anschließend zeichnest du die Ergebnisse auf, um die Auswirkungen der verschiedenen Einstellungen zu beobachten. Die meisten Beispiele verwenden eine 10-Qubit-Schaltung, um Visualisierungen zu erleichtern, und am Ende kannst du den Workflow auf 50 Qubits skalieren.
Dies sind die Fehlerunterdrückungs- und Minderungsoptionen, die du verwenden wirst:
- Dynamical Decoupling
- Messfehlerkompensation
- Gate Twirling
- Zero-Noise Extrapolation (ZNE)
Anforderungen
Stelle vor Beginn dieses Walkthroughs sicher, dass du Folgendes installiert hast:
- Qiskit SDK v2.1 oder höher, mit Unterstützung für Visualisierung
- Qiskit Runtime v0.40 oder höher (
pip install qiskit-ibm-runtime)
Einrichtung
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-ibm-runtime
import matplotlib.pyplot as plt
import numpy as np
from qiskit.circuit.library import efficient_su2, unitary_overlap
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import Batch, EstimatorV2 as Estimator
Schritt 1: Klassische Eingaben auf ein Quantenproblem abbilden
Dieser Walkthrough geht davon aus, dass das klassische Problem bereits auf Quantenmechanik abgebildet wurde. Beginne mit der Konstruktion einer Schaltung und einer Observable zum Messen. Während die hier verwendeten Techniken auf viele verschiedene Arten von Schaltungen anwendbar sind, verwendet dieser Walkthrough der Einfachheit halber die efficient_su2-Schaltung aus der Qiskit-Schaltungsbibliothek.
efficient_su2 ist eine parametrisierte Quantenschaltung, die so konzipiert ist, dass sie auf Quantenhardware mit begrenzter Qubit-Konnektivität effizient ausführbar ist und dennoch ausdrucksstark genug, um Probleme in Anwendungsdomänen wie Optimierung und Chemie zu lösen. Sie wird durch abwechselnde Schichten von parametrisierten Ein-Qubit-Gates mit einer Schicht konstruiert, die ein festes Muster von Zwei-Qubit-Gates enthält, für eine gewählte Anzahl von Wiederholungen. Das Muster der Zwei-Qubit-Gates kann vom Benutzer spezifiziert werden. Hier kannst du das eingebaute pairwise-Muster verwenden, da es die Schaltungstiefe minimiert, indem es die Zwei-Qubit-Gates so dicht wie möglich packt. Dieses Muster kann nur mit linearer Qubit-Konnektivität ausgeführt werden.
n_qubits = 10
reps = 1
circuit = efficient_su2(n_qubits, entanglement="pairwise", reps=reps)
circuit.decompose().draw("mpl", scale=0.7)


Für unsere Observable nehmen wir den Pauli--Operator, der auf das letzte Qubit wirkt, .
# Z on the last qubit (index -1) with coefficient 1.0
observable = SparsePauliOp.from_sparse_list(
[("Z", [-1], 1.0)], num_qubits=n_qubits
)
An diesem Punkt könntest du mit der Ausführung deiner Schaltung fortfahren und die Observable messen. Du möchtest jedoch auch die Ausgabe des Quantengeräts mit der korrekten Antwort vergleichen – das heißt, dem theoretischen Wert der Observable, wenn die Schaltung ohne Fehler ausgeführt worden wäre. Für kleine Quantenschaltungen kannst du diesen Wert berechnen, indem du die Schaltung auf einem klassischen Computer simulierst, aber dies ist für größere Utility-Scale-Schaltungen nicht möglich. Du kannst dieses Problem mit der "Spiegelschaltungs"-Technik (auch bekannt als "Compute-Uncompute") umgehen, die zum Benchmarking der Leistung von Quantengeräten nützlich ist.
Spiegelschaltung
Bei der Spiegelschaltungstechnik verkettest du die Schaltung mit ihrer inversen Schaltung, die durch Invertierung jedes Gates der Schaltung in umgekehrter Reihenfolge gebildet wird. Die resultierende Schaltung implementiert den Identitätsoperator, der trivial simuliert werden kann. Da die Struktur der ursprünglichen Schaltung in der Spiegelschaltung erhalten bleibt, gibt die Ausführung der Spiegelschaltung dennoch eine Vorstellung davon, wie das Quantengerät bei der ursprünglichen Schaltung abschneiden würde.
Die folgende Codezelle weist deiner Schaltung zufällige Parameter zu und konstruiert dann die Spiegelschaltung unter Verwendung der unitary_overlap-Klasse. Füge vor dem Spiegeln der Schaltung eine Barrier-Instruktion hinzu, um zu verhindern, dass der Transpiler die beiden Teile der Schaltung auf beiden Seiten der Barrier zusammenführt. Ohne die Barrier würde der Transpiler die ursprüngliche Schaltung mit ihrer Inversen zusammenführen, was zu einer transpilierten Schaltung ohne Gates führt.
# Generate random parameters
rng = np.random.default_rng(1234)
params = rng.uniform(-np.pi, np.pi, size=circuit.num_parameters)
# Assign the parameters to the circuit
assigned_circuit = circuit.assign_parameters(params)
# Add a barrier to prevent circuit optimization of mirrored operators
assigned_circuit.barrier()
# Construct mirror circuit
mirror_circuit = unitary_overlap(assigned_circuit, assigned_circuit)
mirror_circuit.decompose().draw("mpl", scale=0.7)


Schritt 2: Problem für die Ausführung auf Quantenhardware optimieren
Du musst deine Schaltung optimieren, bevor du sie auf Hardware ausführst. Dieser Prozess umfasst einige Schritte:
- Wähle ein Qubit-Layout, das die virtuellen Qubits deiner Schaltung auf physische Qubits auf der Hardware abbildet.
- Füge nach Bedarf Swap-Gates ein, um Interaktionen zwischen Qubits zu routen, die nicht verbunden sind.
- Übersetze die Gates in deiner Schaltung in Instruction Set Architecture (ISA)-Instruktionen, die direkt auf der Hardware ausgeführt werden können.
- Führe Schaltungsoptimierungen durch, um die Schaltungstiefe und Gate-Anzahl zu minimieren.
Der in Qiskit eingebaute Transpiler kann all diese Schritte für dich durchführen. Da dieses Beispiel eine hardwareeffiziente Schaltung verwendet, sollte der Transpiler in der Lage sein, ein Qubit-Layout zu wählen, das keine Swap-Gates zum Routing von Interaktionen erfordert.
Du musst das zu verwendende Hardwaregerät auswählen, bevor du deine Schaltung optimierst. Die folgende Codezelle fordert das am wenigsten ausgelastete Gerät mit mindestens 127 Qubits an.
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=127
)
Du kannst deine Schaltung für dein gewähltes Backend transpilieren, indem du einen Pass-Manager erstellst und dann den Pass-Manager auf der Schaltung ausführst. Eine einfache Möglichkeit, einen Pass-Manager zu erstellen, ist die Verwendung der Funktion generate_preset_pass_manager. Siehe Transpilierung mit Pass-Managern für eine detailliertere Erklärung der Transpilierung mit Pass-Managern.
pass_manager = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=1234
)
isa_circuit = pass_manager.run(mirror_circuit)
isa_circuit.draw("mpl", idle_wires=False, scale=0.7, fold=-1)


Die transpilierte Schaltung enthält jetzt nur noch ISA-Instruktionen. Die Ein-Qubit-Gates wurden in Bezug auf -Gates und -Rotationen zerlegt, und die CX-Gates wurden in ECR-Gates und Ein-Qubit-Rotationen zerlegt.
Der Transpilationsprozess hat die virtuellen Qubits der Schaltung auf physische Qubits auf der Hardware abgebildet. Die Informationen über das Qubit-Layout sind im layout-Attribut der transpilierten Schaltung gespeichert. Die Observable wurde auch in Bezug auf die virtuellen Qubits definiert, daher musst du dieses Layout auf die Observable anwenden, was du mit der Methode apply_layout von SparsePauliOp tun kannst.
isa_observable = observable.apply_layout(isa_circuit.layout)
print("Original observable:")
print(observable)
print()
print("Observable with layout applied:")
print(isa_observable)
Original observable:
SparsePauliOp(['ZIIIIIIIII'],
coeffs=[1.+0.j])
Observable with layout applied:
SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j])
Schritt 3: Ausführung mit Qiskit Primitives
Du bist nun bereit, deine Schaltung mit dem Estimator-Primitive auszuführen.
Hier reichst du fünf separate Jobs ein, beginnend ohne Fehlerunterdrückung oder -minderung, und aktivierst sukzessive verschiedene Fehlerunterdrückungs- und -minderungsoptionen, die in Qiskit Runtime verfügbar sind. Informationen zu den Optionen findest du auf den folgenden Seiten:
- Übersicht über alle Optionen
- Dynamical Decoupling
- Resilience, einschließlich Messfehlerkompensation und Zero-Noise Extrapolation (ZNE)
- Twirling
Da diese Jobs unabhängig voneinander ausgeführt werden können, kannst du den Batch-Modus verwenden, um Qiskit Runtime die Optimierung des Timings ihrer Ausführung zu ermöglichen.
pub = (isa_circuit, isa_observable)
jobs = []
with Batch(backend=backend) as batch:
estimator = Estimator(mode=batch)
# Set number of shots
estimator.options.default_shots = 100_000
# Disable runtime compilation and error mitigation
estimator.options.resilience_level = 0
# Run job with no error mitigation
job0 = estimator.run([pub])
jobs.append(job0)
# Add dynamical decoupling (DD)
estimator.options.dynamical_decoupling.enable = True
estimator.options.dynamical_decoupling.sequence_type = "XpXm"
job1 = estimator.run([pub])
jobs.append(job1)
# Add readout error mitigation (DD + TREX)
estimator.options.resilience.measure_mitigation = True
job2 = estimator.run([pub])
jobs.append(job2)
# Add gate twirling (DD + TREX + Gate Twirling)
estimator.options.twirling.enable_gates = True
estimator.options.twirling.num_randomizations = "auto"
job3 = estimator.run([pub])
jobs.append(job3)
# Add zero-noise extrapolation (DD + TREX + Gate Twirling + ZNE)
estimator.options.resilience.zne_mitigation = True
estimator.options.resilience.zne.noise_factors = (1, 3, 5)
estimator.options.resilience.zne.extrapolator = ("exponential", "linear")
job4 = estimator.run([pub])
jobs.append(job4)
Schritt 4: Nachbearbeitung und Rückgabe des Ergebnisses im gewünschten klassischen Format
Schließlich kannst du die Daten analysieren. Hier rufst du die Jobergebnisse ab, extrahierst die gemessenen Erwartungswerte aus ihnen und zeichnest die Werte auf, einschließlich Fehlerbalken von einer Standardabweichung.
# Retrieve the job results
results = [job.result() for job in jobs]
# Unpack the PUB results (there's only one PUB result in each job result)
pub_results = [result[0] for result in results]
# Unpack the expectation values and standard errors
expectation_vals = np.array(
[float(pub_result.data.evs) for pub_result in pub_results]
)
standard_errors = np.array(
[float(pub_result.data.stds) for pub_result in pub_results]
)
# Plot the expectation values
fig, ax = plt.subplots()
labels = ["No mitigation", "+ DD", "+ TREX", "+ Twirling", "+ ZNE"]
ax.bar(
range(len(labels)),
expectation_vals,
yerr=standard_errors,
label="experiment",
)
ax.axhline(y=1.0, color="gray", linestyle="--", label="ideal")
ax.set_xticks(range(len(labels)))
ax.set_xticklabels(labels)
ax.set_ylabel("Expectation value")
ax.legend(loc="upper left")
plt.show()
In diesem kleinen Maßstab ist es schwierig, die Wirkung der meisten Fehlerminderungstechniken zu sehen, aber Zero-Noise Extrapolation bietet eine spürbare Verbesserung. Beachte jedoch, dass diese Verbesserung nicht kostenlos kommt, da das ZNE-Ergebnis auch einen größeren Fehlerbalken aufweist.
Skalierung des Experiments nach oben
Bei der Entwicklung eines Experiments ist es nützlich, mit einer kleinen Schaltung zu beginnen, um Visualisierungen und Simulationen zu erleichtern. Nachdem du deinen Workflow auf einer 10-Qubit-Schaltung entwickelt und getestet hast, kannst du ihn auf 50 Qubits skalieren. Die folgende Codezelle wiederholt alle Schritte in diesem Walkthrough, wendet sie aber nun auf eine 50-Qubit-Schaltung an.
n_qubits = 50
reps = 1
# Construct circuit and observable
circuit = efficient_su2(n_qubits, entanglement="pairwise", reps=reps)
observable = SparsePauliOp.from_sparse_list(
[("Z", [-1], 1.0)], num_qubits=n_qubits
)
# Assign parameters to circuit
params = rng.uniform(-np.pi, np.pi, size=circuit.num_parameters)
assigned_circuit = circuit.assign_parameters(params)
assigned_circuit.barrier()
# Construct mirror circuit
mirror_circuit = unitary_overlap(assigned_circuit, assigned_circuit)
# Transpile circuit and observable
isa_circuit = pass_manager.run(mirror_circuit)
isa_observable = observable.apply_layout(isa_circuit.layout)
# Run jobs
pub = (isa_circuit, isa_observable)
jobs = []
with Batch(backend=backend) as batch:
estimator = Estimator(mode=batch)
# Set number of shots
estimator.options.default_shots = 100_000
# Disable runtime compilation and error mitigation
estimator.options.resilience_level = 0
# Run job with no error mitigation
job0 = estimator.run([pub])
jobs.append(job0)
# Add dynamical decoupling (DD)
estimator.options.dynamical_decoupling.enable = True
estimator.options.dynamical_decoupling.sequence_type = "XpXm"
job1 = estimator.run([pub])
jobs.append(job1)
# Add readout error mitigation (DD + TREX)
estimator.options.resilience.measure_mitigation = True
job2 = estimator.run([pub])
jobs.append(job2)
# Add gate twirling (DD + TREX + Gate Twirling)
estimator.options.twirling.enable_gates = True
estimator.options.twirling.num_randomizations = "auto"
job3 = estimator.run([pub])
jobs.append(job3)
# Add zero-noise extrapolation (DD + TREX + Gate Twirling + ZNE)
estimator.options.resilience.zne_mitigation = True
estimator.options.resilience.zne.noise_factors = (1, 3, 5)
estimator.options.resilience.zne.extrapolator = ("exponential", "linear")
job4 = estimator.run([pub])
jobs.append(job4)
# Retrieve the job results
results = [job.result() for job in jobs]
# Unpack the PUB results (there's only one PUB result in each job result)
pub_results = [result[0] for result in results]
# Unpack the expectation values and standard errors
expectation_vals = np.array(
[float(pub_result.data.evs) for pub_result in pub_results]
)
standard_errors = np.array(
[float(pub_result.data.stds) for pub_result in pub_results]
)
# Plot the expectation values
fig, ax = plt.subplots()
labels = ["No mitigation", "+ DD", "+ TREX", "+ Twirling", "+ ZNE"]
ax.bar(
range(len(labels)),
expectation_vals,
yerr=standard_errors,
label="experiment",
)
ax.axhline(y=1.0, color="gray", linestyle="--", label="ideal")
ax.set_xticks(range(len(labels)))
ax.set_xticklabels(labels)
ax.set_ylabel("Expectation value")
ax.legend(loc="upper left")
plt.show()
Wenn du die 50-Qubit-Ergebnisse mit den 10-Qubit-Ergebnissen von vorher vergleichst, wirst du möglicherweise Folgendes feststellen (deine Ergebnisse können zwischen den Läufen variieren):
- Die Ergebnisse ohne Fehlerminderung sind schlechter. Die Ausführung der größeren Schaltung beinhaltet die Ausführung von mehr Gates, sodass es mehr Möglichkeiten gibt, dass sich Fehler ansammeln.
- Die Hinzufügung von Dynamical Decoupling könnte die Leistung verschlechtert haben. Dies ist nicht überraschend, da die Schaltung sehr dicht ist. Dynamical Decoupling ist hauptsächlich nützlich, wenn es große Lücken in der Schaltung gibt, während derer Qubits ohne angewendete Gates im Leerlauf sitzen. Wenn diese Lücken nicht vorhanden sind, ist Dynamical Decoupling nicht effektiv und kann die Leistung tatsächlich verschlechtern, aufgrund von Fehlern in den Dynamical-Decoupling-Pulsen selbst. Die 10-Qubit-Schaltung war möglicherweise zu klein, um diesen Effekt zu beobachten.
- Mit Zero-Noise Extrapolation ist das Ergebnis so gut oder fast so gut wie das 10-Qubit-Ergebnis, obwohl der Fehlerbalken viel größer ist. Dies demonstriert die Leistungsfähigkeit der ZNE-Technik!
Fazit
In diesem Walkthrough hast du verschiedene Fehlerminderungsoptionen untersucht, die für das Qiskit Runtime Estimator-Primitive verfügbar sind. Du hast einen Workflow mit einer 10-Qubit-Schaltung entwickelt und ihn dann auf 50 Qubits skaliert. Du hast möglicherweise beobachtet, dass die Aktivierung von mehr Fehlerunterdrückungs- und -minderungsoptionen nicht immer die Leistung verbessert (insbesondere die Aktivierung von Dynamical Decoupling in diesem Fall). Die meisten Optionen akzeptieren zusätzliche Konfigurationen, die du in deiner eigenen Arbeit testen kannst!