Przejdź do głównej treści

Benchmarking w czasie rzeczywistym dla wyboru Qubitów

Szacowane użycie: 4 minuty na procesorze Eagle r2 (UWAGA: to jest tylko szacunek. Czas wykonania może się różnić.)

# 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

Tło

Ten samouczek pokazuje, jak przeprowadzać eksperymenty charakteryzacji w czasie rzeczywistym i aktualizować właściwości backendu, aby poprawić wybór Qubitów podczas mapowania Circuit na fizyczne Qubity w QPU. Dowiesz się, jakie podstawowe eksperymenty charakteryzacji służą do wyznaczania właściwości QPU, jak je przeprowadzać w Qiskit oraz jak aktualizować właściwości zapisane w obiekcie backendu reprezentującym QPU na podstawie tych eksperymentów.

Właściwości raportowane przez QPU są aktualizowane raz dziennie, jednak system może dryfować szybciej niż wynosi odstęp między aktualizacjami. Może to wpływać na wiarygodność procedur wyboru Qubitów w etapie Layout menedżera przejść, ponieważ korzystałyby one z raportowanych właściwości, które nie odzwierciedlają aktualnego stanu QPU. Z tego powodu może być warto poświęcić część czasu QPU na eksperymenty charakteryzacji, których wyniki można następnie wykorzystać do aktualizacji właściwości QPU używanych przez procedurę Layout.

Wymagania

Przed rozpoczęciem tego samouczka upewnij się, że masz zainstalowane następujące elementy:

  • Qiskit SDK v2.0 lub nowszy, z obsługą wizualizacji
  • Qiskit Runtime v0.40 lub nowszy ( pip install qiskit-ibm-runtime )
  • Qiskit Experiments v0.12 lub nowszy ( pip install qiskit-experiments )
  • Biblioteka grafowa Rustworkx (pip install rustworkx)

Konfiguracja

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

Krok 1: Mapowanie klasycznych danych wejściowych na problem kwantowy

Aby zmierzyć różnicę w wydajności, rozważamy Circuit przygotowujący stan Bella wzdłuż liniowego łańcucha o zmiennej długości. Mierzona jest wierność stanu Bella na końcach łańcucha.

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)

Output of the previous code cell

Output of the previous code cell

Konfiguracja backendu i mapy sprzężeń

Najpierw wybierz backend

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

Następnie pobierz jego mapę sprzężeń

coupling_graph = backend.coupling_map.graph.to_undirected(multigraph=False)

# Get unidirectional coupling map
one_dir_coupling_map = coupling_graph.edge_list()

Aby jednocześnie testować jak najwięcej bramek dwuQubitowych, dzielimy mapę sprzężeń na layered_coupling_map. Obiekt ten zawiera listę warstw, gdzie każda warstwa jest listą krawędzi, na których można jednocześnie wykonywać bramki dwuQubitowe. Nazywa się to również kolorowaniem krawędzi mapy sprzężeń.

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

Eksperymenty charakteryzacji

Do scharakteryzowania głównych właściwości Qubitów w QPU używana jest seria eksperymentów. Są to T1T_1, T2T_2, błąd odczytu oraz błąd bramki jednoQubitowej i dwuQubitowej. Krótko podsumujemy, czym są te właściwości i odniesiemy się do eksperymentów z pakietu qiskit-experiments, które służą do ich charakteryzacji.

T1

T1T_1 to charakterystyczny czas, po którym wzbudzony Qubit opada do stanu podstawowego na skutek procesów dekoherencji tłumienia amplitudy. W eksperymencie T1T_1 mierzymy wzbudzony Qubit po określonym opóźnieniu. Im większy czas opóźnienia, tym bardziej prawdopodobne jest, że Qubit opadnie do stanu podstawowego. Celem eksperymentu jest scharakteryzowanie szybkości zaniku Qubitu do stanu podstawowego.

T2

T2T_2 reprezentuje czas potrzebny do tego, aby rzut wektora Blocha pojedynczego Qubitu na płaszczyznę XY spadł do około 37% (1e\frac{1}{e}) swojej początkowej amplitudy na skutek procesów dekoherencji depfazowania. W eksperymencie echa Hahna T2T_2 możemy oszacować szybkość tego zaniku.

Charakteryzacja błędów przygotowania stanu i pomiaru (SPAM)

W eksperymencie charakteryzacji błędów SPAM Qubity są przygotowywane w określonym stanie (0\vert 0 \rangle lub 1\vert 1 \rangle) i mierzone. Prawdopodobieństwo zmierzenia stanu innego niż przygotowany daje wówczas prawdopodobieństwo błędu.

Jednobitowe i dwubitowe randomizowane benchmarki

Randomizowane benchmarkowanie (RB) to popularny protokół służący do charakteryzacji częstości błędów procesorów kwantowych. Eksperyment RB polega na generowaniu losowych Circuitów Clifforda na danych Qubitach w taki sposób, aby obliczona przez te Circuity macierz unitarna była macierzą jednostkową. Po uruchomieniu Circuitów zliczana jest liczba pomiarów skutkujących błędem (tzn. wynikiem różnym od stanu podstawowego), a na podstawie tych danych wnioskuje się o szacunkach błędów urządzenia kwantowego przez obliczenie błędu na bramkę Clifforda.

# 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,
)

Właściwości QPU w czasie

Analizując raportowane właściwości QPU w czasie (poniżej rozpatrzymy jeden tydzień), widzimy, jak mogą się one wahać w skali jednego dnia. Niewielkie wahania mogą zachodzić nawet w ciągu jednego dnia. W takim scenariuszu raportowane właściwości (aktualizowane raz dziennie) nie będą dokładnie odzwierciedlać bieżącego stanu QPU. Co więcej, jeśli zadanie jest transpilowane lokalnie (z użyciem aktualnie raportowanych właściwości) i przesłane, ale wykonane dopiero później (za minuty lub dni), istnieje ryzyko, że do wyboru Qubitów w kroku transpilacji użyto nieaktualnych właściwości. Podkreśla to znaczenie posiadania aktualnych informacji o QPU w momencie wykonywania. Najpierw pobierzmy właściwości w określonym przedziale czasu.

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)

Następnie narysujmy wartości na wykresie

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

Output of the previous code cell

Widać, że na przestrzeni kilku dni niektóre właściwości Qubitów mogą się znacznie zmieniać. Podkreśla to znaczenie posiadania aktualnych informacji o stanie QPU, aby móc wybrać najlepiej działające Qubity do eksperymentu.

Krok 2: Optymalizacja problemu pod kątem wykonania na sprzęcie kwantowym

W tym samouczku nie wykonujemy żadnej optymalizacji układów ani operatorów.

Krok 3: Wykonanie z użyciem prymitywów Qiskit

Wykonanie układu kwantowego z domyślnym wyborem Qubitów

Jako punkt odniesienia dla oceny wydajności wykonamy układ kwantowy na QPU przy użyciu domyślnych Qubitów — czyli tych wybranych na podstawie właściwości zgłoszonych przez backend. Użyjemy optimization_level = 3. To ustawienie obejmuje najbardziej zaawansowane optymalizacje transpilacji i wykorzystuje właściwości docelowe (takie jak błędy operacji) do wyboru najlepiej działających Qubitów do wykonania.

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
]

Wykonanie układu kwantowego z wyborem Qubitów w czasie rzeczywistym

W tej sekcji zbadamy znaczenie aktualnych informacji o właściwościach Qubitów QPU dla uzyskania optymalnych wyników. Najpierw przeprowadzimy pełny zestaw eksperymentów charakteryzacyjnych QPU (T1T_1, T2T_2, SPAM, jednoQubitowe RB i dwuQubitowe RB), których wyniki posłużą do aktualizacji właściwości backendu. Dzięki temu pass manager może dobierać Qubity do wykonania na podstawie świeżych informacji o QPU, co może poprawić wydajność wykonania. Następnie wykonamy układ pary Bella i porównamy wierność uzyskaną przy wyborze Qubitów na podstawie zaktualizowanych właściwości QPU z wiernością uzyskaną wcześniej, gdy do wyboru Qubitów w etapie transpilacji używano domyślnie zgłoszonych właściwości.

ostrożnie

Pamiętaj, że niektóre eksperymenty charakteryzacyjne mogą się nie powieść, gdy procedura dopasowywania krzywej nie jest w stanie dopasować krzywej do zmierzonych danych. Jeśli widzisz ostrzeżenia pochodzące z tych eksperymentów, sprawdź je, aby zrozumieć, które charakteryzacje zawiodły na których Qubitach, i spróbuj dostosować parametry eksperymentu (na przykład czasy dla T1T_1, T2T_2 lub długości sekwencji w eksperymentach RB).

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

Krok 4: Przetwarzanie końcowe i zwracanie wyników w żądanym formacie klasycznym

Na koniec porównajmy wierność stanu Bella uzyskaną w dwóch różnych wariantach:

  • original — z domyślnymi Qubitami wybranymi przez Transpiler na podstawie zgłoszonych właściwości backendu.
  • updated — z Qubitami wybranymi na podstawie zaktualizowanych właściwości backendu po przeprowadzeniu eksperymentów charakteryzacyjnych.
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()

Output of the previous code cell

Nie każde uruchomienie wykaże poprawę wydajności dzięki charakteryzacji w czasie rzeczywistym — a wraz ze wzrostem długości łańcucha, a co za tym idzie mniejszą swobodą w doborze fizycznych Qubitów, znaczenie aktualnych informacji o urządzeniu maleje. Niemniej jednak dobrą praktyką jest zbieranie świeżych danych o właściwościach urządzenia, aby rozumieć jego bieżącą wydajność. Okazjonalnie przejściowe układy dwupoziomowe mogą wpływać na działanie niektórych Qubitów. Dane zebrane w czasie rzeczywistym informują nas, kiedy takie zdarzenia mają miejsce, i pomagają unikać niepowodzeń eksperymentalnych w takich przypadkach.

Wezwanie do działania

Spróbuj zastosować tę metodę we własnych eksperymentach i sprawdź, jakie korzyści przynosi! Możesz też sprawdzić, jak duże ulepszenia uzyskasz na różnych backendach.

Ankieta dotycząca samouczka

Wypełnij tę krótką ankietę, aby podzielić się opinią na temat tego samouczka. Twoje uwagi pomogą nam poprawić ofertę treści i jakość doświadczeń użytkownika.

Link do ankiety

Note: This survey is provided by IBM Quantum and relates to the original English content. To give feedback on doQumentation's website, translations, or code execution, please open a GitHub issue.