Echtzeit-Benchmarking zur Qubit-Auswahl
Geschätzte Nutzungsdauer: 4 Minuten auf einem Eagle r2 Prozessor (HINWEIS: Dies ist nur eine Schätzung. Deine tatsächliche Laufzeit kann abweichen.)
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-experiments qiskit-ibm-runtime rustworkx
# This cell is hidden from users – it disables some lint rules
# ruff: noqa: E722
Hintergrund
Dieses Tutorial zeigt, wie du Echtzeit-Charakterisierungsexperimente durchführst und Backend-Eigenschaften aktualisierst, um die Qubit-Auswahl beim Mapping eines Circuits auf die physischen Qubits eines QPU zu verbessern. Du lernst die grundlegenden Charakterisierungsexperimente kennen, mit denen Eigenschaften des QPU bestimmt werden, wie du diese in Qiskit durchführst und wie du die im Backend-Objekt gespeicherten Eigenschaften des QPU auf Basis dieser Experimente aktualisierst.
Die vom QPU gemeldeten Eigenschaften werden einmal täglich aktualisiert, aber das System kann schneller driften als das Intervall zwischen den Aktualisierungen. Das kann die Zuverlässigkeit der Qubit-Auswahlroutinen in der Layout-Phase des Pass Managers beeinträchtigen, da diese gemeldete Eigenschaften verwenden würden, die den aktuellen Zustand des QPU nicht widerspiegeln. Aus diesem Grund kann es sinnvoll sein, etwas QPU-Zeit für Charakterisierungsexperimente zu verwenden, die dann zur Aktualisierung der vom Layout-Routine genutzten QPU-Eigenschaften genutzt werden können.
Voraussetzungen
Stelle vor Beginn dieses Tutorials sicher, dass folgendes installiert ist:
- Qiskit SDK v2.0 oder neuer, mit Unterstützung für Visualisierung
- Qiskit Runtime v0.40 oder neuer (
pip install qiskit-ibm-runtime) - Qiskit Experiments v0.12 oder neuer (
pip install qiskit-experiments) - Rustworkx Graph-Bibliothek (
pip install rustworkx)
Setup
from qiskit_ibm_runtime import SamplerV2
from qiskit.transpiler import generate_preset_pass_manager
from qiskit.quantum_info import hellinger_fidelity
from qiskit.transpiler import InstructionProperties
from qiskit_experiments.library import (
T1,
T2Hahn,
LocalReadoutError,
StandardRB,
)
from qiskit_experiments.framework import BatchExperiment, ParallelExperiment
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import Session
from datetime import datetime
from collections import defaultdict
import numpy as np
import rustworkx
import matplotlib.pyplot as plt
import copy
Schritt 1: Klassische Eingaben auf ein Quantenproblem abbilden
Um den Leistungsunterschied zu messen, betrachten wir einen Circuit, der einen Bell-Zustand über eine lineare Kette verschiedener Längen vorbereitet. Die Fidelität des Bell-Zustands an den Enden der Kette wird gemessen.
from qiskit import QuantumCircuit
ideal_dist = {"00": 0.5, "11": 0.5}
num_qubits_list = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 127]
circuits = []
for num_qubits in num_qubits_list:
circuit = QuantumCircuit(num_qubits, 2)
circuit.h(0)
for i in range(num_qubits - 1):
circuit.cx(i, i + 1)
circuit.barrier()
circuit.measure(0, 0)
circuit.measure(num_qubits - 1, 1)
circuits.append(circuit)
circuits[-1].draw(output="mpl", style="clifford", fold=-1)


Backend und Coupling Map einrichten
Wähle zunächst ein Backend aus:
# To run on hardware, select the backend with the fewest number of jobs in the queue
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=127
)
qubits = list(range(backend.num_qubits))
Hole dann die Coupling Map ab:
coupling_graph = backend.coupling_map.graph.to_undirected(multigraph=False)
# Get unidirectional coupling map
one_dir_coupling_map = coupling_graph.edge_list()
Um möglichst viele Zwei-Qubit-Gates gleichzeitig zu benchmarken, unterteilen wir die Coupling Map in eine layered_coupling_map. Dieses Objekt enthält eine Liste von Schichten, wobei jede Schicht eine Liste von Kanten ist, auf denen Zwei-Qubit-Gates gleichzeitig ausgeführt werden können. Dies wird auch als Kantenfärbung der Coupling Map bezeichnet.
# Get layered coupling map
edge_coloring = rustworkx.graph_bipartite_edge_color(coupling_graph)
layered_coupling_map = defaultdict(list)
for edge_idx, color in edge_coloring.items():
layered_coupling_map[color].append(
coupling_graph.get_edge_endpoints_by_index(edge_idx)
)
layered_coupling_map = [
sorted(layered_coupling_map[i])
for i in sorted(layered_coupling_map.keys())
]
Charakterisierungsexperimente
Eine Reihe von Experimenten dient dazu, die wesentlichen Eigenschaften der Qubits in einem QPU zu charakterisieren. Dazu gehören , , Auslesefehler sowie Einzel-Qubit- und Zwei-Qubit-Gate-Fehler. Im Folgenden fassen wir kurz zusammen, was diese Eigenschaften bedeuten, und verweisen auf Experimente im Paket qiskit-experiments, die zu ihrer Charakterisierung verwendet werden.
T1
ist die charakteristische Zeit, die ein angeregtes Qubit benötigt, um aufgrund von Amplitudendämpfungs-Dekohärenzprozessen in den Grundzustand zurückzufallen. In einem -Experiment wird ein angeregtes Qubit nach einer Verzögerung gemessen. Je länger die Verzögerungszeit, desto wahrscheinlicher fällt das Qubit in den Grundzustand. Ziel des Experiments ist es, die Zerfallsrate des Qubits in Richtung des Grundzustands zu charakterisieren.
T2
gibt die Zeitspanne an, die benötigt wird, damit die Projektion des Bloch-Vektors eines einzelnen Qubits auf die XY-Ebene aufgrund von Dephasierungs-Dekohärenzprozessen auf etwa 37 % () seiner Anfangsamplitude abfällt. In einem -Hahn-Echo-Experiment kann die Rate dieses Zerfalls abgeschätzt werden.
Charakterisierung von Zustandspräparations- und Messfehlern (SPAM)
In einem SPAM-Fehler-Charakterisierungsexperiment werden Qubits in einem bestimmten Zustand ( oder ) präpariert und gemessen. Die Wahrscheinlichkeit, einen anderen als den präparierten Zustand zu messen, ergibt dann die Fehlerwahrscheinlichkeit.
Einzel-Qubit- und Zwei-Qubit-Randomized-Benchmarking
Randomized Benchmarking (RB) ist ein weit verbreitetes Protokoll zur Charakterisierung der Fehlerrate von Quantenprozessoren. Ein RB-Experiment besteht aus der Erzeugung zufälliger Clifford-Circuits auf den angegebenen Qubits, sodass die von den Circuits berechnete unitäre Transformation die Identität ergibt. Nach der Ausführung der Circuits wird die Anzahl der Shots gezählt, die zu einem Fehler führen (d. h. zu einer Ausgabe, die vom Grundzustand abweicht). Aus diesen Daten lassen sich Fehlerabschätzungen für das Quantengerät ableiten, indem der Fehler pro Clifford berechnet wird.
# Create T1 experiments on all qubit in parallel
t1_exp = ParallelExperiment(
[
T1(
physical_qubits=[qubit],
delays=[1e-6, 20e-6, 40e-6, 80e-6, 200e-6, 400e-6],
)
for qubit in qubits
],
backend,
analysis=None,
)
# Create T2-Hahn experiments on all qubit in parallel
t2_exp = ParallelExperiment(
[
T2Hahn(
physical_qubits=[qubit],
delays=[1e-6, 20e-6, 40e-6, 80e-6, 200e-6, 400e-6],
)
for qubit in qubits
],
backend,
analysis=None,
)
# Create readout experiments on all qubit in parallel
readout_exp = LocalReadoutError(qubits)
# Create single-qubit RB experiments on all qubit in parallel
singleq_rb_exp = ParallelExperiment(
[
StandardRB(
physical_qubits=[qubit], lengths=[10, 100, 500], num_samples=10
)
for qubit in qubits
],
backend,
analysis=None,
)
# Create two-qubit RB experiments on the three layers of disjoint edges of the heavy-hex
twoq_rb_exp_batched = BatchExperiment(
[
ParallelExperiment(
[
StandardRB(
physical_qubits=pair,
lengths=[10, 50, 100],
num_samples=10,
)
for pair in layer
],
backend,
analysis=None,
)
for layer in layered_coupling_map
],
backend,
flatten_results=True,
analysis=None,
)
QPU-Eigenschaften im Zeitverlauf
Betrachtet man die gemeldeten QPU-Eigenschaften über die Zeit (wir betrachten unten eine einzelne Woche), sieht man, wie stark diese im Maßstab eines einzelnen Tages schwanken können. Kleine Schwankungen können sogar innerhalb eines Tages auftreten. In diesem Szenario spiegeln die gemeldeten Eigenschaften (einmal täglich aktualisiert) den aktuellen Zustand des QPU nicht genau wider. Wenn ein Job zudem lokal transpiliert (unter Verwendung der aktuell gemeldeten Eigenschaften) und eingereicht wird, aber erst zu einem späteren Zeitpunkt (Minuten oder Tage später) ausgeführt wird, besteht das Risiko, dass bei der Transpilierung veraltete Eigenschaften für die Qubit-Auswahl verwendet wurden. Dies unterstreicht die Bedeutung aktueller Informationen über den QPU zum Ausführungszeitpunkt. Rufen wir zunächst die Eigenschaften über einen bestimmten Zeitraum ab.
instruction_2q_name = "cz" # set the name of the default 2q of the device
errors_list = []
for day_idx in range(10, 17):
calibrations_time = datetime(
year=2025, month=8, day=day_idx, hour=0, minute=0, second=0
)
targer_hist = backend.target_history(datetime=calibrations_time)
t1_dict, t2_dict = {}, {}
for qubit in range(targer_hist.num_qubits):
t1_dict[qubit] = targer_hist.qubit_properties[qubit].t1
t2_dict[qubit] = targer_hist.qubit_properties[qubit].t2
errors_dict = {
"1q": targer_hist["sx"],
"2q": targer_hist[f"{instruction_2q_name}"],
"spam": targer_hist["measure"],
"t1": t1_dict,
"t2": t2_dict,
}
errors_list.append(errors_dict)
Dann stellen wir die Werte grafisch dar:
fig, axs = plt.subplots(5, 1, figsize=(10, 20), sharex=False)
# Plot for T1 values
for qubit in range(targer_hist.num_qubits):
t1s = []
for errors_dict in errors_list:
t1_dict = errors_dict["t1"]
try:
t1s.append(t1_dict[qubit] / 1e-6)
except:
print(f"missing t1 data for qubit {qubit}")
axs[0].plot(t1s)
axs[0].set_title("T1")
axs[0].set_ylabel(r"Time ($\mu s$)")
axs[0].set_xlabel("Days")
# Plot for T2 values
for qubit in range(targer_hist.num_qubits):
t2s = []
for errors_dict in errors_list:
t2_dict = errors_dict["t2"]
try:
t2s.append(t2_dict[qubit] / 1e-6)
except:
print(f"missing t2 data for qubit {qubit}")
axs[1].plot(t2s)
axs[1].set_title("T2")
axs[1].set_ylabel(r"Time ($\mu s$)")
axs[1].set_xlabel("Days")
# Plot SPAM values
for qubit in range(targer_hist.num_qubits):
spams = []
for errors_dict in errors_list:
spam_dict = errors_dict["spam"]
spams.append(spam_dict[tuple([qubit])].error)
axs[2].plot(spams)
axs[2].set_title("SPAM Errors")
axs[2].set_ylabel("Error Rate")
axs[2].set_xlabel("Days")
# Plot 1Q Gate Errors
for qubit in range(targer_hist.num_qubits):
oneq_gates = []
for errors_dict in errors_list:
oneq_gate_dict = errors_dict["1q"]
oneq_gates.append(oneq_gate_dict[tuple([qubit])].error)
axs[3].plot(oneq_gates)
axs[3].set_title("1Q Gate Errors")
axs[3].set_ylabel("Error Rate")
axs[3].set_xlabel("Days")
# Plot 2Q Gate Errors
for pair in one_dir_coupling_map:
twoq_gates = []
for errors_dict in errors_list:
twoq_gate_dict = errors_dict["2q"]
twoq_gates.append(twoq_gate_dict[pair].error)
axs[4].plot(twoq_gates)
axs[4].set_title("2Q Gate Errors")
axs[4].set_ylabel("Error Rate")
axs[4].set_xlabel("Days")
plt.subplots_adjust(hspace=0.5)
plt.show()

Man sieht, dass sich einige Qubit-Eigenschaften über mehrere Tage hinweg deutlich verändern können. Das unterstreicht, wie wichtig es ist, aktuelle Informationen über den Zustand des QPU zu haben, um die am besten geeigneten Qubits für ein Experiment auswählen zu können.
Schritt 2: Problem für die Ausführung auf Quantenhardware optimieren
In diesem Tutorial wird keine Optimierung der Circuits oder Operatoren vorgenommen.
Schritt 3: Ausführung mit Qiskit-Primitiven
Quantencircuit mit Standard-Qubit-Auswahl ausführen
Als Referenzwert führen wir einen Quantencircuit auf einem QPU mit den Standard-Qubits aus, also den Qubits, die anhand der gemeldeten Backend-Eigenschaften ausgewählt wurden. Wir verwenden optimization_level = 3. Diese Einstellung umfasst die fortschrittlichsten Transpilierungsoptimierungen und nutzt Zieleigenschaften (wie Operationsfehler), um die am besten geeigneten Qubits für die Ausführung auszuwählen.
pm = generate_preset_pass_manager(target=backend.target, optimization_level=3)
isa_circuits = pm.run(circuits)
initial_qubits = [
[
idx
for idx, qb in circuit.layout.initial_layout.get_physical_bits().items()
if qb._register.name != "ancilla"
]
for circuit in isa_circuits
]
Quantencircuit mit Echtzeit-Qubit-Auswahl ausführen
In diesem Abschnitt untersuchen wir, wie wichtig aktuelle Informationen über die Qubit-Eigenschaften des QPU für optimale Ergebnisse sind. Zunächst führen wir eine vollständige Suite von QPU-Charakterisierungsexperimenten durch (, , SPAM, Einzel-Qubit-RB und Zwei-Qubit-RB), mit denen wir anschließend die Backend-Eigenschaften aktualisieren. Dadurch kann der Pass Manager die Qubits für die Ausführung auf Basis aktueller Informationen über den QPU auswählen, was die Ausführungsleistung möglicherweise verbessert. Anschließend führen wir den Bell-Paar-Circuit aus und vergleichen die Fidelität, die mit aktualisierten QPU-Eigenschaften bei der Qubit-Auswahl erzielt wird, mit der Fidelität, die zuvor mit den standardmäßig gemeldeten Eigenschaften erreicht wurde.
Beachte, dass einige Charakterisierungsexperimente fehlschlagen können, wenn die Fitting-Routine keine Kurve an die gemessenen Daten anpassen kann. Wenn du Warnungen von diesen Experimenten siehst, untersuche sie, um herauszufinden, welche Charakterisierung bei welchen Qubits fehlgeschlagen ist, und versuche, die Parameter des Experiments anzupassen (z. B. die Zeiten für , oder die Längen der RB-Experimente).
# Prepare characterization experiments
batches = [t1_exp, t2_exp, readout_exp, singleq_rb_exp, twoq_rb_exp_batched]
batches_exp = BatchExperiment(batches, backend) # , analysis=None)
run_options = {"shots": 1e3, "dynamic": False}
with Session(backend=backend) as session:
sampler = SamplerV2(mode=session)
# Run characterization experiments
batches_exp_data = batches_exp.run(
sampler=sampler, **run_options
).block_for_results()
EPG_sx_result_list = batches_exp_data.analysis_results("EPG_sx")
EPG_sx_result_q_indices = [
result.device_components.index for result in EPG_sx_result_list
]
EPG_x_result_list = batches_exp_data.analysis_results("EPG_x")
EPG_x_result_q_indices = [
result.device_components.index for result in EPG_x_result_list
]
T1_result_list = batches_exp_data.analysis_results("T1")
T1_result_q_indices = [
result.device_components.index for result in T1_result_list
]
T2_result_list = batches_exp_data.analysis_results("T2")
T2_result_q_indices = [
result.device_components.index for result in T2_result_list
]
Readout_result_list = batches_exp_data.analysis_results(
"Local Readout Mitigator"
)
EPG_2q_result_list = batches_exp_data.analysis_results(
f"EPG_{instruction_2q_name}"
)
# Update target properties
target = copy.deepcopy(backend.target)
for i in range(target.num_qubits - 1):
qarg = (i,)
if qarg in EPG_sx_result_q_indices:
target.update_instruction_properties(
instruction="sx",
qargs=qarg,
properties=InstructionProperties(
error=EPG_sx_result_list[i].value.nominal_value
),
)
if qarg in EPG_x_result_q_indices:
target.update_instruction_properties(
instruction="x",
qargs=qarg,
properties=InstructionProperties(
error=EPG_x_result_list[i].value.nominal_value
),
)
err_mat = Readout_result_list.value.assignment_matrix(i)
readout_assignment_error = (
err_mat[0, 1] + err_mat[1, 0]
) / 2 # average readout error
target.update_instruction_properties(
instruction="measure",
qargs=qarg,
properties=InstructionProperties(error=readout_assignment_error),
)
if qarg in T1_result_q_indices:
target.qubit_properties[i].t1 = T1_result_list[
i
].value.nominal_value
if qarg in T2_result_q_indices:
target.qubit_properties[i].t2 = T2_result_list[
i
].value.nominal_value
for pair_idx, pair in enumerate(one_dir_coupling_map):
qarg = tuple(pair)
try:
target.update_instruction_properties(
instruction=instruction_2q_name,
qargs=qarg,
properties=InstructionProperties(
error=EPG_2q_result_list[pair_idx].value.nominal_value
),
)
except:
target.update_instruction_properties(
instruction=instruction_2q_name,
qargs=qarg[::-1],
properties=InstructionProperties(
error=EPG_2q_result_list[pair_idx].value.nominal_value
),
)
# transpile circuits to updated target
pm = generate_preset_pass_manager(target=target, optimization_level=3)
isa_circuit_updated = pm.run(circuits)
updated_qubits = [
[
idx
for idx, qb in circuit.layout.initial_layout.get_physical_bits().items()
if qb._register.name != "ancilla"
]
for circuit in isa_circuit_updated
]
n_trials = 3 # run multiple trials to see variations
# interleave circuits
interleaved_circuits = []
for original_circuit, updated_circuit in zip(
isa_circuits, isa_circuit_updated
):
interleaved_circuits.append(original_circuit)
interleaved_circuits.append(updated_circuit)
# Run circuits
# Set simple error suppression/mitigation options
sampler.options.dynamical_decoupling.enable = True
sampler.options.dynamical_decoupling.sequence_type = "XY4"
job_interleaved = sampler.run(interleaved_circuits * n_trials)
Schritt 4: Nachverarbeitung und Ausgabe im gewünschten klassischen Format
Abschließend vergleichen wir die Fidelität des Bell-Zustands in den beiden verschiedenen Szenarien:
original: mit den Standard-Qubits, die der Transpiler auf Basis der gemeldeten Backend-Eigenschaften ausgewählt hat.updated: mit den Qubits, die auf Basis der aktualisierten Backend-Eigenschaften nach den Charakterisierungsexperimenten ausgewählt wurden.
results = job_interleaved.result()
all_fidelity_list, all_fidelity_updated_list = [], []
for exp_idx in range(n_trials):
fidelity_list, fidelity_updated_list = [], []
for idx, num_qubits in enumerate(num_qubits_list):
pub_result_original = results[
2 * exp_idx * len(num_qubits_list) + 2 * idx
]
pub_result_updated = results[
2 * exp_idx * len(num_qubits_list) + 2 * idx + 1
]
fid = hellinger_fidelity(
ideal_dist, pub_result_original.data.c.get_counts()
)
fidelity_list.append(fid)
fid_up = hellinger_fidelity(
ideal_dist, pub_result_updated.data.c.get_counts()
)
fidelity_updated_list.append(fid_up)
all_fidelity_list.append(fidelity_list)
all_fidelity_updated_list.append(fidelity_updated_list)
plt.figure(figsize=(8, 6))
plt.errorbar(
num_qubits_list,
np.mean(all_fidelity_list, axis=0),
yerr=np.std(all_fidelity_list, axis=0),
fmt="o-.",
label="original",
color="b",
)
# plt.plot(num_qubits_list, fidelity_list, '-.')
plt.errorbar(
num_qubits_list,
np.mean(all_fidelity_updated_list, axis=0),
yerr=np.std(all_fidelity_updated_list, axis=0),
fmt="o-.",
label="updated",
color="r",
)
# plt.plot(num_qubits_list, fidelity_updated_list, '-.')
plt.xlabel("Chain length")
plt.xticks(num_qubits_list)
plt.ylabel("Fidelity")
plt.title("Bell pair fidelity at the edge of N-qubits chain")
plt.legend()
plt.grid(
alpha=0.2,
linestyle="-.",
)
plt.show()

Nicht jeder Durchlauf wird dank der Echtzeit-Charakterisierung eine Leistungsverbesserung zeigen – und mit zunehmender Kettenlänge und damit geringerem Spielraum bei der Auswahl physischer Qubits wird die Bedeutung aktueller Geräteinformationen weniger ausschlaggebend. Es ist jedoch gute Praxis, aktuelle Daten über die Geräteeigenschaften zu erfassen, um die Leistung besser einschätzen zu können. Gelegentlich können transiente Zwei-Niveau-Systeme die Leistung einiger Qubits beeinträchtigen. Echtzeit-Daten können uns darüber informieren, wann solche Ereignisse auftreten, und uns helfen, experimentelle Fehler in solchen Fällen zu vermeiden.
Wende diese Methode auf deine eigenen Ausführungen an und finde heraus, wie viel Verbesserung du dabei erzielst! Du kannst auch testen, welche Verbesserungen du mit verschiedenen Backends erreichst.
Tutorial-Umfrage
Bitte nimm dir einen Moment Zeit für diese kurze Umfrage, um Feedback zu diesem Tutorial zu geben. Deine Rückmeldungen helfen uns, unsere Inhalte und die Nutzererfahrung zu verbessern.