Benchmarking in tempo reale per la selezione dei qubit
Stima dell'utilizzo: 4 minuti con un processore Eagle r2 (NOTA: questa è solo una stima. Il tempo di esecuzione potrebbe variare)
Sfondo
Questa esercitazione mostra come eseguire esperimenti di caratterizzazione in tempo reale e aggiornare le proprietà del backend per migliorare la selezione dei qubit durante la mappatura di un circuito sui qubit fisici di una QPU. Imparerete gli esperimenti di caratterizzazione di base utilizzati per determinare le proprietà della QPU, come eseguirli in Qiskit e come aggiornare le proprietà salvate nell'oggetto backend che rappresenta la QPU in base a questi esperimenti.
Le proprietà riportate dal QPU vengono aggiornate una volta al giorno, ma il sistema può andare alla deriva più velocemente del tempo che intercorre tra un aggiornamento e l'altro. Questo può influire sull'affidabilità delle routine di selezione dei qubit nella fase Layout del gestore di passaggi, poiché utilizzerebbero proprietà riportate che non rappresentano lo stato attuale della QPU. Per questo motivo, può valere la pena di dedicare un po' di tempo alla QPU per effettuare esperimenti di caratterizzazione, che possono poi essere utilizzati per aggiornare le proprietà della QPU utilizzate dalla routine Layout .
Requisiti
Prima di iniziare questa esercitazione, assicuratevi di aver installato quanto segue:
- Qiskit SDK v2.0 o versioni successive, con supporto alla visualizzazione
- Qiskit Runtime v0.40 o più tardi (
pip install qiskit-ibm-runtime) - Esperimenti Qiskit v0.12 o successivo (
pip install qiskit-experiments) - Libreria di grafi Rustworkx (
pip install rustworkx)
Configura
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 copyFase 1: mappare gli input classici su un problema quantistico
Per valutare la differenza di prestazioni, consideriamo un circuito che prepara uno stato Bell attraverso una catena lineare di lunghezza variabile. Si misura la fedeltà dello stato di Bell alle estremità della catena.
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:
Configurazione del backend e della mappa di accoppiamento
Per prima cosa, selezionare un 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))Si ottiene quindi la sua mappa di accoppiamento
coupling_graph = backend.coupling_map.graph.to_undirected(multigraph=False)
# Get unidirectional coupling map
one_dir_coupling_map = coupling_graph.edge_list()Per poter effettuare il benchmark del maggior numero possibile di porte a due qubit contemporaneamente, separiamo la mappa di accoppiamento in una layered_coupling_map. Questo oggetto contiene un elenco di strati, dove ogni strato è un elenco di bordi su cui possono essere eseguite contemporaneamente porte a due qubit. Questa è anche chiamata colorazione dei bordi della mappa di accoppiamento.
# 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())
]Esperimenti di caratterizzazione
Una serie di esperimenti viene utilizzata per caratterizzare le principali proprietà dei qubit in una QPU. Questi sono , , l'errore di lettura e l'errore di gate a singolo e a due bit. Riassumeremo brevemente quali sono queste proprietà e faremo riferimento agli esperimenti nel pacchetto qiskit-experiments utilizzati per caratterizzarle.
T1
è il tempo caratteristico che impiega un qubit eccitato a cadere allo stato terreno a causa dei processi di decoerenza con smorzamento dell'ampiezza. In un esperimento , misuriamo un qubit eccitato dopo un ritardo. Quanto più grande è il tempo di ritardo, tanto più probabilità che il qubit cada allo stato di massa. L'obiettivo dell'esperimento è quello di caratterizzare il tasso di decadimento del qubit verso lo stato di terra stato fondamentale.
T2
rappresenta la quantità di tempo necessaria affinché la proiezione del vettore Bloch di un singolo qubit sul piano XY scenda a circa il 37% ( ) della sua ampiezza iniziale a causa dei processi di decoerenza con dephasing della sua ampiezza iniziale a causa dei processi di decoerenza con dephasing. In un esperimento di eco di Hahn , possiamo stimare la velocità di questo decadimento.
Caratterizzazione dell'errore di preparazione e misurazione dello stato (SPAM)
In un esperimento di caratterizzazione degli errori SPAM, i qubit vengono preparati in un determinato stato ( o ) e misurati. La probabilità di misurare uno stato diverso da quello preparato dà quindi la probabilità dell'errore.
Benchmarking randomizzato a singolo qubit e a due qubit
Il benchmarking randomizzato (RB) è un protocollo popolare per caratterizzare il tasso di errore dei processori quantistici processori quantistici. Un esperimento di RB consiste nella generazione di circuiti Clifford sui qubit dati, in modo che l'unitario calcolato dai circuiti sia l'identità identità. Dopo aver eseguito i circuiti, si conta il numero di scatti che danno luogo a un errore (cioè un'uscita diversa dallo stato fondamentale) e da questi dati si possono dedurre le stime di errore per il dispositivo quantistico, calcolando l'Errore per Clifford.
# 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,
)Proprietà QPU nel tempo
Osservando le proprietà QPU riportate nel corso del tempo (di seguito prenderemo in considerazione una singola settimana), vediamo come queste possano fluttuare su una scala di un singolo giorno. Piccole fluttuazioni possono verificarsi anche nell'arco di un giorno. In questo scenario, le proprietà segnalate (aggiornate una volta al giorno) non cattureranno accuratamente lo stato attuale del QPU. Inoltre, se un lavoro viene transpilato localmente (utilizzando le proprietà correnti riportate) e inviato ma eseguito solo in un momento successivo (minuti o giorni), può correre il rischio di aver utilizzato proprietà obsolete per la selezione dei qubit nella fase di transpilatura. Ciò evidenzia l'importanza di disporre di informazioni aggiornate sulla QPU al momento dell'esecuzione. Per prima cosa, recuperiamo le proprietà in un determinato intervallo di tempo.
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)Quindi, tracciamo i valori
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:
Si può notare che nell'arco di diversi giorni alcune proprietà dei qubit possono cambiare considerevolmente. Ciò evidenzia l'importanza di avere informazioni fresche sullo stato della QPU, per poter selezionare i qubit più performanti per un esperimento.
Fase 2: Ottimizzazione del problema per l'esecuzione su hardware quantistico
In questa esercitazione non viene effettuata alcuna ottimizzazione dei circuiti o degli operatori.
Passaggio 3: eseguire utilizzando Qiskit primitives
Eseguire un circuito quantistico con selezione predefinita dei qubit
Come risultato di riferimento per le prestazioni, eseguiremo un circuito quantistico su una QPU utilizzando i qubit di default, ovvero i qubit selezionati con le proprietà del backend richieste. Utilizzeremo optimization_level = 3. Questa impostazione include l'ottimizzazione più avanzata della transpilazione e utilizza le proprietà del target (come gli errori di operazione) per selezionare i qubit più performanti per l'esecuzione.
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
]Eseguire un circuito quantistico con selezione dei qubit in tempo reale
In questa sezione analizzeremo l'importanza di avere informazioni aggiornate sulle proprietà dei qubit della QPU per ottenere risultati ottimali. Per prima cosa, effettueremo una serie completa di esperimenti di caratterizzazione delle QPU ( , , SPAM, RB a un solo qubit e RB a due qubit), che potremo poi utilizzare per aggiornare le proprietà del backend. Ciò consente al gestore del passaggio di selezionare i qubit per l'esecuzione in base a nuove informazioni sulla QPU, migliorando eventualmente le prestazioni dell'esecuzione. In secondo luogo, eseguiamo il circuito a coppie di Bell e confrontiamo la fedeltà ottenuta dopo aver selezionato i qubit con le proprietà aggiornate della QPU con la fedeltà ottenuta in precedenza quando si utilizzano le proprietà riportate di default per la selezione dei qubit.
Si noti che alcuni degli esperimenti di caratterizzazione potrebbero fallire quando la routine di adattamento non riesce ad adattare una curva ai dati misurati. Se vedete avvisi provenienti da questi esperimenti, ispezionateli per capire quale caratterizzazione è fallita su quali qubit e provate a regolare i parametri dell'esperimento (come i tempi per , , o il numero di lunghezze degli esperimenti 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)Fase 4: Post-elaborazione e restituzione del risultato nel formato classico desiderato
Infine, confrontiamo la fedeltà dello stato di Bell ottenuto nelle due diverse impostazioni:
originalcioè con i qubit predefiniti scelti dal transpiler in base alle proprietà segnalate del backend.updatedcioè con i qubit scelti in base alle proprietà aggiornate del backend dopo l'esecuzione degli esperimenti di caratterizzazione.
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:
Non tutte le esecuzioni mostreranno un miglioramento delle prestazioni grazie alla caratterizzazione in tempo reale e con l'aumento della lunghezza della catena, e quindi con una minore libertà di scelta dei qubit fisici, l'importanza delle informazioni aggiornate sul dispositivo diventa meno sostanziale. Tuttavia, è buona norma raccogliere nuovi dati sulle proprietà del dispositivo per comprenderne le prestazioni. Occasionalmente, i sistemi transitori a due livelli possono influenzare le prestazioni di alcuni qubit. I dati in tempo reale possono informarci quando si verificano tali eventi e aiutarci a evitare fallimenti sperimentali in questi casi.
Provate ad applicare questo metodo alle vostre esecuzioni e verificate quanti vantaggi ottenete! Si può anche provare a vedere i miglioramenti ottenuti da diversi backend.
Sondaggio tutorial
Vi invitiamo a partecipare a questo breve sondaggio per fornire un feedback su questa esercitazione. Le vostre indicazioni ci aiuteranno a migliorare la nostra offerta di contenuti e l'esperienza degli utenti.