Benchmarking em tempo real para seleção de qubits
Estimativa de uso: 4 minutos em um processador Eagle r2 (NOTA: Esta é apenas uma estimativa. Seu tempo de execução pode variar.)
# 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
Contexto
Este tutorial mostra como executar experimentos de caracterização em tempo real e atualizar as propriedades do backend para melhorar a seleção de qubits ao mapear um circuito para os qubits físicos em uma QPU. Você aprenderá os experimentos básicos de caracterização que são usados para determinar propriedades da QPU, como fazê-los no Qiskit e como atualizar as propriedades salvas no objeto backend que representa a QPU com base nesses experimentos.
As propriedades relatadas pela QPU são atualizadas uma vez por dia, mas o sistema pode derivar mais rápido do que o tempo entre as atualizações. Isso pode afetar a confiabilidade das rotinas de seleção de qubits no estágio Layout do gerenciador de passos, pois estariam usando propriedades relatadas que não representam o estado presente da QPU. Por essa razão, pode valer a pena dedicar algum tempo da QPU a experimentos de caracterização, que podem então ser usados para atualizar as propriedades da QPU utilizadas pela rotina Layout.
Requisitos
Antes de iniciar este tutorial, certifique-se de ter o seguinte instalado:
- Qiskit SDK v2.0 ou posterior, com suporte a visualização
- Qiskit Runtime v0.40 ou posterior (
pip install qiskit-ibm-runtime) - Qiskit Experiments v0.12 ou posterior (
pip install qiskit-experiments) - Biblioteca de grafos Rustworkx (
pip install rustworkx)
Configuração
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
Passo 1: Mapear entradas clássicas para um problema quântico
Para avaliar a diferença no desempenho, consideramos um circuito que prepara um estado de Bell através de uma cadeia linear de comprimento variável. A fidelidade do estado de Bell nas extremidades da cadeia é medida.
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)


Configurar backend e mapa de acoplamento
Primeiro, selecione um 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))
Então obtenha seu mapa de acoplamento
coupling_graph = backend.coupling_map.graph.to_undirected(multigraph=False)
# Get unidirectional coupling map
one_dir_coupling_map = coupling_graph.edge_list()
Para avaliar o maior número possível de portas de dois qubits simultaneamente, separamos o mapa de acoplamento em um layered_coupling_map. Este objeto contém uma lista de camadas onde cada camada é uma lista de arestas nas quais portas de dois qubits podem ser executadas ao mesmo tempo. Isso também é chamado de coloração de arestas do mapa de acoplamento.
# 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())
]
Experimentos de caracterização
Uma série de experimentos é usada para caracterizar as principais propriedades dos qubits em uma QPU. São elas , , erro de leitura e erro de portas de um qubit e dois qubits. Resumiremos brevemente o que essas propriedades são e nos referiremos a experimentos no pacote qiskit-experiments que são usados para caracterizá-las.
T1
é o tempo característico que um qubit excitado leva para cair para o estado fundamental devido a processos de decoerência por amortecimento de amplitude. Em um experimento de , medimos um qubit excitado após um atraso. Quanto maior é o tempo de atraso, mais provável é que o qubit caia para o estado fundamental. O objetivo do experimento é caracterizar a taxa de decaimento do qubit em direção ao estado fundamental.
T2
representa a quantidade de tempo necessária para que a projeção do vetor de Bloch de um único qubit no plano XY caia para aproximadamente 37% () de sua amplitude inicial devido a processos de decoerência por defasagem. Em um experimento de Eco de Hahn , podemos estimar a taxa desse decaimento.
Caracterização de erro de preparação de estado e medição (SPAM)
Em um experimento de caracterização de erro SPAM, os qubits são preparados em um determinado estado ( ou ) e medidos. A probabilidade de medir um estado diferente daquele preparado então fornece a probabilidade do erro.
Benchmarking randomizado de um qubit e dois qubits
Benchmarking randomizado (RB) é um protocolo popular para caracterizar a taxa de erro de processadores quânticos. Um experimento RB consiste na geração de circuitos Clifford aleatórios nos qubits dados de modo que o unitário computado pelos circuitos seja a identidade. Após executar os circuitos, o número de shots resultando em um erro (ou seja, uma saída diferente do estado fundamental) são contados, e a partir desses dados pode-se inferir estimativas de erro para o dispositivo quântico, calculando o Erro Por 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,
)
Propriedades da QPU ao longo do tempo
Observando as propriedades relatadas da QPU ao longo do tempo (consideraremos uma única semana abaixo), vemos como estas podem flutuar na escala de um único dia. Pequenas flutuações podem acontecer até mesmo dentro de um dia. Neste cenário, as propriedades relatadas (atualizadas uma vez por dia) não capturarão com precisão o status atual da QPU. Além disso, se um trabalho for transpilado localmente (usando as propriedades relatadas atuais) e enviado, mas executado apenas em um momento posterior (minutos ou dias), pode correr o risco de ter usado propriedades desatualizadas para a seleção de qubits no passo de transpilação. Isso destaca a importância de ter informações atualizadas sobre a QPU no momento da execução. Primeiro, vamos recuperar as propriedades ao longo de um determinado intervalo de 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)
Então, vamos plotar os valores
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()

Você pode ver que ao longo de vários dias algumas das propriedades dos qubits podem mudar consideravelmente. Isso destaca a importância de ter informações atualizadas do status da QPU, para poder selecionar os qubits de melhor desempenho para um experimento.
Passo 2: Otimizar o problema para execução em hardware quântico
Nenhuma otimização dos circuitos ou operadores é feita neste tutorial.
Passo 3: Executar usando primitivas Qiskit
Executar um circuito quântico com seleção de qubit padrão
Como resultado de referência de desempenho, executaremos um circuito quântico em uma QPU usando os qubits padrão, que são os qubits selecionados com as propriedades de backend solicitadas. Usaremos optimization_level = 3. Esta configuração inclui a otimização de transpilação mais avançada e usa propriedades de destino (como erros de operação) para selecionar os qubits com melhor desempenho para execução.
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
]
Executar um circuito quântico com seleção de qubit em tempo real
Nesta seção, investigaremos a importância de ter informações atualizadas sobre as propriedades dos qubits da QPU para obter resultados ótimos. Primeiro, realizaremos um conjunto completo de experimentos de caracterização da QPU (, , SPAM, RB de um qubit e RB de dois qubits), que podemos então usar para atualizar as propriedades do backend. Isso permite que o gerenciador de passes selecione qubits para execução com base em informações atualizadas sobre a QPU, possivelmente melhorando o desempenho da execução. Em segundo lugar, executamos o circuito de par de Bell e comparamos a fidelidade obtida após selecionar os qubits com propriedades de QPU atualizadas com a fidelidade que obtivemos antes, quando usamos as propriedades padrão reportadas para seleção de qubits.
Observe que alguns dos experimentos de caracterização podem falhar quando a rotina de ajuste não consegue ajustar uma curva aos dados medidos. Se você ver avisos provenientes desses experimentos, inspecione-os para entender qual caracterização falhou em quais qubits e tente ajustar os parâmetros do experimento (como os tempos para , , ou o número de comprimentos dos experimentos 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)
Passo 4: Pós-processar e retornar o resultado no formato clássico desejado
Finalmente, vamos comparar a fidelidade do estado de Bell obtida nas duas configurações diferentes:
original, ou seja, com os qubits padrão escolhidos pelo transpilador com base nas propriedades reportadas do backend.updated, ou seja, com os qubits escolhidos com base nas propriedades atualizadas do backend após a execução dos experimentos de caracterização.
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()

Nem todas as execuções mostrarão melhoria no desempenho devido à caracterização em tempo real - e com o aumento do comprimento da cadeia, e portanto menos liberdade para escolher qubits físicos, a importância das informações atualizadas do dispositivo se torna menos substancial. No entanto, é uma boa prática coletar dados atualizados sobre as propriedades do dispositivo para entender seu desempenho. Ocasionalmente, sistemas de dois níveis transitórios podem afetar o desempenho de alguns dos qubits. Dados em tempo real podem nos informar quando tais eventos estão acontecendo e nos ajudar a evitar falhas experimentais nesses casos.
Tente aplicar este método às suas execuções e determine quanto benefício você obtém! Você também pode tentar e ver quantas melhorias você obtém de diferentes backends.
Pesquisa do tutorial
Por favor, responda a esta breve pesquisa para fornecer feedback sobre este tutorial. Suas percepções nos ajudarão a melhorar nossas ofertas de conteúdo e experiência do usuário.