Kombination von Fehlerminderungsoptionen mit dem Estimator-Primitive
Nutzungsschätzung: 7 Minuten auf einem Heron r2-Prozessor (HINWEIS: Dies ist nur eine Schätzung. Deine Laufzeit kann variieren.)
Lernziele
Wir empfehlen, dass Nutzer mit den folgenden Themen vertraut sind, bevor sie dieses Tutorial durcharbeiten:
- Die Grundlagen von Dynamical Decoupling, Messfehlerkompensation, Gate Twirling und Zero-Noise Extrapolation, wie in diesem Leitfaden beschrieben.
Voraussetzungen
Nach dem Durcharbeiten dieses Tutorials sollten Nutzer verstehen:
- Wie die oben genannten Fehlerminderungstechniken selektiv auf Hardware implementiert werden.
- Wie sie sich in Bezug auf ihre Fähigkeit, Hardware-Rauschen zu mindern, vergleichen.
Hintergrund
Dieses Tutorial untersucht die Fehlerunterdrückungs- und Fehlerminderungsoptionen, die mit dem Estimator-Primitive von Qiskit Runtime verfügbar sind. Dieses Tutorial zeigt, wie jede der folgenden Methoden einzeln implementiert wird:
- Dynamical Decoupling
- Messfehlerkompensation
- Gate Twirling
- Zero-Noise Extrapolation (ZNE)
Beachte, dass eine Alternative zur individuellen Implementierung dieser Techniken deren Implementierung über ein Resilience-Level ist, wobei resilience_level die Werte 0, 1, 2 annimmt:
- 0 : Es wird keine Minderung implementiert.
- 1 : Messfehlerkompensation wird implementiert.
- 2 : Gate Twirling, Messfehlerkompensation und ZNE werden implementiert.
In diesem Tutorial wirst du 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. Der größte Teil des Tutorials verwendet eine 10-Qubit-Schaltung, um Visualisierungen zu erleichtern, und am Ende skalierst du den Workflow auf 50 Qubits.
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
Kleinskaliges Simulator-Beispiel
Wir überspringen diesen Schritt, da Runtime-Fehlerminderung auf Simulatoren nicht unterstützt wird.
Hardware-Beispiel
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 aufgebaut, 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)

Als unsere Observable nehmen wir den Pauli--Operator, der auf das letzte Qubit wirkt, . Beachte, dass die Tatsache, dass das letzte Qubit dem ersten Element dieser Zeichenkette entspricht, auf Qiskits Verwendung der Little-Endian-Notation zurückzuführen ist.
# 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 und so eine transpilierte Schaltung ohne Gates erzeugt.
# 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
)
print(backend)
<IBMBackend('ibm_fez')>
Du kannst deine Schaltung auf 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. Alle Gates wurden in Bezug auf -Gates und -Rotationen sowie CZ-Gates 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(['IIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
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)
estimator.options.environment.job_tags = [
"TUT_CEM_SS"
] # add tag for this small scale job
# 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.
Großskaliges Hardware-Beispiel
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)
estimator.options.environment.job_tags = [
"TUT_CEM_LS"
] # add tag for this large scale job
# 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):
- Alle Experimente liefern Ergebnisse, die näher am idealen Wert liegen, und alle Fehlerbalken sind kleiner.
- Die Hinzufügung von Dynamical Decoupling könnte die Leistung im Vergleich zum Fall ohne Minderung 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 liegt das Ergebnis sehr nahe am idealen Wert. Dies demonstriert die Leistungsfähigkeit von ZNE.
Nächste Schritte
Falls du diese Arbeit interessant findest, könntest du an folgendem Material über einige zusätzliche Fehlerminderungs- und Fehlerunterdrückungstechniken interessiert sein, die in diesem Tutorial nicht erwähnt wurden: